feat(ui): 第二版样式
This commit is contained in:
parent
55eb0e643b
commit
724a5aca31
|
|
@ -1,14 +1,9 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { Message } from "@langchain/langgraph-sdk";
|
|
||||||
import type { UseStream } from "@langchain/langgraph-sdk/react";
|
|
||||||
import { FilesIcon, ListTodoIcon, XIcon } from "lucide-react";
|
import { FilesIcon, ListTodoIcon, XIcon } from "lucide-react";
|
||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
||||||
|
|
||||||
import { ConversationEmptyState } from "@/components/ai-elements/conversation";
|
import { ConversationEmptyState } from "@/components/ai-elements/conversation";
|
||||||
import { usePromptInputController } from "@/components/ai-elements/prompt-input";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
DevDialog,
|
DevDialog,
|
||||||
|
|
@ -23,12 +18,12 @@ import {
|
||||||
ArtifactFileList,
|
ArtifactFileList,
|
||||||
useArtifacts,
|
useArtifacts,
|
||||||
} from "@/components/workspace/artifacts";
|
} from "@/components/workspace/artifacts";
|
||||||
|
import { useThreadChat } from "@/components/workspace/chats";
|
||||||
import { DevTodoList } from "@/components/workspace/dev-todo-list";
|
import { DevTodoList } from "@/components/workspace/dev-todo-list";
|
||||||
import { InputBox } from "@/components/workspace/input-box";
|
import { InputBox } from "@/components/workspace/input-box";
|
||||||
import { MessageList } from "@/components/workspace/messages";
|
import { MessageList } from "@/components/workspace/messages";
|
||||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||||
import { ThreadTitle } from "@/components/workspace/thread-title";
|
import { ThreadTitle } from "@/components/workspace/thread-title";
|
||||||
import { TodoList } from "@/components/workspace/todo-list";
|
|
||||||
import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator";
|
import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator";
|
||||||
import { Tooltip } from "@/components/workspace/tooltip";
|
import { Tooltip } from "@/components/workspace/tooltip";
|
||||||
import { useSpecificChatMode } from "@/components/workspace/use-chat-mode";
|
import { useSpecificChatMode } from "@/components/workspace/use-chat-mode";
|
||||||
|
|
@ -36,25 +31,14 @@ import { Welcome } from "@/components/workspace/welcome";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { useNotification } from "@/core/notification/hooks";
|
import { useNotification } from "@/core/notification/hooks";
|
||||||
import { useLocalSettings } from "@/core/settings";
|
import { useLocalSettings } from "@/core/settings";
|
||||||
import { bootstrapRemoteSkill } from "@/core/skills";
|
import { useThreadStream } from "@/core/threads/hooks";
|
||||||
import { type AgentThread, type AgentThreadState } from "@/core/threads";
|
import { pathOfThread, textOfMessage } from "@/core/threads/utils";
|
||||||
import {
|
|
||||||
useSubmitThread,
|
|
||||||
useThreadStreamLegacy as useThreadStream,
|
|
||||||
} from "@/core/threads/hooks";
|
|
||||||
import {
|
|
||||||
pathOfThread,
|
|
||||||
textOfMessage,
|
|
||||||
titleOfThread,
|
|
||||||
} from "@/core/threads/utils";
|
|
||||||
import { uuid } from "@/core/utils/uuid";
|
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
import { useSelectedSkillListener } from "@/hooks/use-selected-skill-listener";
|
import { useSelectedSkillListener } from "@/hooks/use-selected-skill-listener";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const router = useRouter();
|
|
||||||
useSpecificChatMode();
|
useSpecificChatMode();
|
||||||
const [settings, setSettings] = useLocalSettings();
|
const [settings, setSettings] = useLocalSettings();
|
||||||
const { setOpen: setSidebarOpen } = useSidebar();
|
const { setOpen: setSidebarOpen } = useSidebar();
|
||||||
|
|
@ -67,106 +51,48 @@ export default function ChatPage() {
|
||||||
selectedArtifact,
|
selectedArtifact,
|
||||||
fullscreen,
|
fullscreen,
|
||||||
} = useArtifacts();
|
} = useArtifacts();
|
||||||
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
|
const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat();
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const promptInputController = usePromptInputController();
|
|
||||||
|
|
||||||
// UI mode depends only on route: /workspace/chats/new is always "new page" mode.
|
|
||||||
const isNewThread = useMemo(
|
|
||||||
() => threadIdFromPath === "new",
|
|
||||||
[threadIdFromPath],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Submission strategy is controlled by `isnew` query param only.
|
|
||||||
// - isnew=false: reuse existing thread
|
|
||||||
// - otherwise: create/start a new session
|
|
||||||
const createNewSession = useMemo(() => {
|
|
||||||
if (threadIdFromPath !== "new") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return searchParams.get("isnew")?.trim().toLowerCase() !== "false";
|
|
||||||
}, [threadIdFromPath, searchParams]);
|
|
||||||
|
|
||||||
const uploadTarget = useMemo(() => {
|
|
||||||
const target = searchParams.get("upload_target")?.trim().toLowerCase();
|
|
||||||
return target === "skill" ? "skill" : undefined;
|
|
||||||
}, [searchParams]);
|
|
||||||
|
|
||||||
const [threadId, setThreadId] = useState<string | null>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
if (threadIdFromPath !== "new") {
|
|
||||||
setThreadId(threadIdFromPath);
|
|
||||||
} else {
|
|
||||||
const queryThreadId = searchParams.get("thread_id")?.trim();
|
|
||||||
setThreadId(queryThreadId ?? uuid());
|
|
||||||
}
|
|
||||||
}, [threadIdFromPath, searchParams]);
|
|
||||||
|
|
||||||
// Runtime strategy for /new page:
|
|
||||||
// - UI remains new-page mode
|
|
||||||
// - if isnew=false, execute against existing thread_id without creating a new one
|
|
||||||
const reuseExistingThread = useMemo(
|
|
||||||
() => threadIdFromPath === "new" && !createNewSession && !!threadId,
|
|
||||||
[threadIdFromPath, createNewSession, threadId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
|
|
||||||
// 监听宿主页 selectedSkill 消息
|
// 监听宿主页 selectedSkill 消息
|
||||||
const {
|
const {
|
||||||
selectedSkill,
|
|
||||||
skillError: selectedSkillError,
|
skillError: selectedSkillError,
|
||||||
clearSkillError: clearSelectedSkillError,
|
clearSkillError: clearSelectedSkillError,
|
||||||
isBootstrapping: isSelectedSkillBootstrapping,
|
isBootstrapping: isSelectedSkillBootstrapping,
|
||||||
} = useSelectedSkillListener({ threadId });
|
} = useSelectedSkillListener({ threadId });
|
||||||
const [finalState, setFinalState] = useState<AgentThreadState | null>(null);
|
const [thread, sendMessage, isUploading] = useThreadStream({
|
||||||
const thread = useThreadStream({
|
threadId: isNewThread ? undefined : threadId,
|
||||||
// Keep UI in new-page mode, but runtime may reuse existing thread
|
context: settings.context,
|
||||||
isNewThread: reuseExistingThread ? false : isNewThread,
|
isMock,
|
||||||
threadId,
|
onStart: (currentThreadId) => {
|
||||||
fetchStateHistory: true,
|
setIsNewThread(false);
|
||||||
|
history.replaceState(null, "", pathOfThread(currentThreadId));
|
||||||
|
},
|
||||||
onFinish: (state) => {
|
onFinish: (state) => {
|
||||||
setFinalState(state);
|
|
||||||
// 新对话完成后导航到对话页面
|
|
||||||
if (isNewThread && threadId) {
|
|
||||||
router.push(pathOfThread(threadId));
|
|
||||||
}
|
|
||||||
if (document.hidden || !document.hasFocus()) {
|
if (document.hidden || !document.hasFocus()) {
|
||||||
let body = "Conversation finished";
|
let body = "Conversation finished";
|
||||||
const lastMessage = state.messages[state.messages.length - 1];
|
const lastMessage = state.messages.at(-1);
|
||||||
if (lastMessage) {
|
if (lastMessage) {
|
||||||
const textContent = textOfMessage(lastMessage);
|
const textContent = textOfMessage(lastMessage);
|
||||||
if (textContent) {
|
if (textContent) {
|
||||||
if (textContent.length > 200) {
|
body =
|
||||||
body = textContent.substring(0, 200) + "...";
|
textContent.length > 200
|
||||||
} else {
|
? textContent.substring(0, 200) + "..."
|
||||||
body = textContent;
|
: textContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
showNotification(state.title, { body });
|
||||||
showNotification(state.title, {
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}) as unknown as UseStream<AgentThreadState>;
|
});
|
||||||
useEffect(() => {
|
|
||||||
if (thread.isLoading) setFinalState(null);
|
|
||||||
}, [thread.isLoading]);
|
|
||||||
|
|
||||||
const title = useMemo(() => {
|
const title = useMemo(() => {
|
||||||
let result = isNewThread
|
const result = thread.values?.title ?? "";
|
||||||
? ""
|
return result === "Untitled" ? "" : result;
|
||||||
: titleOfThread(thread as unknown as AgentThread);
|
}, [thread.values?.title]);
|
||||||
if (result === "Untitled") {
|
|
||||||
result = "";
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}, [thread, isNewThread]);
|
|
||||||
|
|
||||||
const [hasSubmitted, setHasSubmitted] = useState(false);
|
const [hasSubmitted, setHasSubmitted] = useState(false);
|
||||||
const suppressExistingThreadPrefetchUi = reuseExistingThread && !hasSubmitted;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const pageTitle = isNewThread
|
const pageTitle = isNewThread
|
||||||
|
|
@ -174,7 +100,7 @@ export default function ChatPage() {
|
||||||
: thread.values?.title && thread.values.title !== "Untitled"
|
: thread.values?.title && thread.values.title !== "Untitled"
|
||||||
? thread.values.title
|
? thread.values.title
|
||||||
: t.pages.untitled;
|
: t.pages.untitled;
|
||||||
if (thread.isThreadLoading && !suppressExistingThreadPrefetchUi) {
|
if (thread.isThreadLoading) {
|
||||||
document.title = `Loading... - ${t.pages.appName}`;
|
document.title = `Loading... - ${t.pages.appName}`;
|
||||||
} else {
|
} else {
|
||||||
document.title = `${pageTitle} - ${t.pages.appName}`;
|
document.title = `${pageTitle} - ${t.pages.appName}`;
|
||||||
|
|
@ -184,9 +110,8 @@ export default function ChatPage() {
|
||||||
t.pages.newChat,
|
t.pages.newChat,
|
||||||
t.pages.untitled,
|
t.pages.untitled,
|
||||||
t.pages.appName,
|
t.pages.appName,
|
||||||
thread.values.title,
|
thread.values?.title,
|
||||||
thread.isThreadLoading,
|
thread.isThreadLoading,
|
||||||
suppressExistingThreadPrefetchUi,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true);
|
const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true);
|
||||||
|
|
@ -215,44 +140,22 @@ export default function ChatPage() {
|
||||||
return artifactsOpen;
|
return artifactsOpen;
|
||||||
}, [artifactsOpen, artifacts]);
|
}, [artifactsOpen, artifacts]);
|
||||||
|
|
||||||
const [todoListCollapsed, setTodoListCollapsed] = useState(true);
|
const todoListCollapsed = true;
|
||||||
const [showExitDialog, setShowExitDialog] = useState(false);
|
const [showExitDialog, setShowExitDialog] = useState(false);
|
||||||
|
|
||||||
const submitThread = useSubmitThread({
|
|
||||||
isNewThread,
|
|
||||||
createNewSession,
|
|
||||||
threadId,
|
|
||||||
thread,
|
|
||||||
uploadTarget,
|
|
||||||
threadContext: {
|
|
||||||
...settings.context,
|
|
||||||
thinking_enabled: settings.context.mode !== "flash",
|
|
||||||
is_plan_mode:
|
|
||||||
settings.context.mode === "pro" || settings.context.mode === "ultra",
|
|
||||||
subagent_enabled: settings.context.mode === "ultra",
|
|
||||||
},
|
|
||||||
afterSubmit() {
|
|
||||||
// 导航已在 onFinish 中处理,确保 stream 完成后再导航
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(message: Parameters<typeof submitThread>[0]) => {
|
(message: Parameters<typeof sendMessage>[1]) => {
|
||||||
if (isSelectedSkillBootstrapping) {
|
if (isSelectedSkillBootstrapping) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setHasSubmitted(true);
|
setHasSubmitted(true);
|
||||||
void submitThread(message);
|
void sendMessage(threadId, message);
|
||||||
},
|
},
|
||||||
[isSelectedSkillBootstrapping, submitThread],
|
[isSelectedSkillBootstrapping, sendMessage, threadId],
|
||||||
);
|
);
|
||||||
const handleStop = useCallback(async () => {
|
const handleStop = useCallback(async () => {
|
||||||
await thread.stop();
|
await thread.stop();
|
||||||
}, [thread]);
|
}, [thread]);
|
||||||
|
|
||||||
if (!threadId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThreadContext.Provider value={{ thread }}>
|
<ThreadContext.Provider value={{ thread }}>
|
||||||
<div
|
<div
|
||||||
|
|
@ -355,14 +258,6 @@ export default function ChatPage() {
|
||||||
)}
|
)}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
thread={thread}
|
thread={thread}
|
||||||
// suppressThreadLoading={suppressExistingThreadPrefetchUi}
|
|
||||||
// messagesOverride={
|
|
||||||
// suppressExistingThreadPrefetchUi
|
|
||||||
// ? []
|
|
||||||
// : !thread.isLoading && finalState?.messages
|
|
||||||
// ? (finalState.messages as Message[])
|
|
||||||
// : undefined
|
|
||||||
// }
|
|
||||||
paddingBottom={todoListCollapsed ? 160 : 280}
|
paddingBottom={todoListCollapsed ? 160 : 280}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -449,13 +344,14 @@ export default function ChatPage() {
|
||||||
>
|
>
|
||||||
<InputBox
|
<InputBox
|
||||||
className={cn("w-full rounded-[20px] bg-[#FBFAFC]")}
|
className={cn("w-full rounded-[20px] bg-[#FBFAFC]")}
|
||||||
|
threadId={threadId}
|
||||||
isNewThread={isNewThread}
|
isNewThread={isNewThread}
|
||||||
hasSubmitted={hasSubmitted}
|
hasSubmitted={hasSubmitted}
|
||||||
autoFocus={isNewThread}
|
autoFocus={isNewThread}
|
||||||
status={
|
status={
|
||||||
suppressExistingThreadPrefetchUi
|
thread.error
|
||||||
? "ready"
|
? "error"
|
||||||
: thread.isLoading
|
: isUploading || thread.isLoading
|
||||||
? "streaming"
|
? "streaming"
|
||||||
: "ready"
|
: "ready"
|
||||||
}
|
}
|
||||||
|
|
@ -469,7 +365,8 @@ export default function ChatPage() {
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
|
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
|
||||||
isSelectedSkillBootstrapping
|
isSelectedSkillBootstrapping ||
|
||||||
|
isUploading
|
||||||
}
|
}
|
||||||
onContextChange={(context) => setSettings("context", context)}
|
onContextChange={(context) => setSettings("context", context)}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function DropdownMenu({
|
function DropdownMenu({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuPortal({
|
function DropdownMenuPortal({
|
||||||
|
|
@ -17,18 +17,20 @@ function DropdownMenuPortal({
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuTrigger({
|
function DropdownMenuTrigger({
|
||||||
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Trigger
|
<DropdownMenuPrimitive.Trigger
|
||||||
data-slot="dropdown-menu-trigger"
|
data-slot="dropdown-menu-trigger"
|
||||||
|
className={cn(className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuContent({
|
function DropdownMenuContent({
|
||||||
|
|
@ -42,13 +44,13 @@ function DropdownMenuContent({
|
||||||
data-slot="dropdown-menu-content"
|
data-slot="dropdown-menu-content"
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-[20px] border p-[20px] shadow-md",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuPrimitive.Portal>
|
</DropdownMenuPrimitive.Portal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuGroup({
|
function DropdownMenuGroup({
|
||||||
|
|
@ -56,7 +58,7 @@ function DropdownMenuGroup({
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuItem({
|
function DropdownMenuItem({
|
||||||
|
|
@ -65,8 +67,8 @@ function DropdownMenuItem({
|
||||||
variant = "default",
|
variant = "default",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
variant?: "default" | "destructive"
|
variant?: "default" | "destructive";
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
|
|
@ -75,11 +77,11 @@ function DropdownMenuItem({
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuCheckboxItem({
|
function DropdownMenuCheckboxItem({
|
||||||
|
|
@ -93,7 +95,7 @@ function DropdownMenuCheckboxItem({
|
||||||
data-slot="dropdown-menu-checkbox-item"
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -105,7 +107,7 @@ function DropdownMenuCheckboxItem({
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuRadioGroup({
|
function DropdownMenuRadioGroup({
|
||||||
|
|
@ -116,7 +118,7 @@ function DropdownMenuRadioGroup({
|
||||||
data-slot="dropdown-menu-radio-group"
|
data-slot="dropdown-menu-radio-group"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuRadioItem({
|
function DropdownMenuRadioItem({
|
||||||
|
|
@ -128,8 +130,8 @@ function DropdownMenuRadioItem({
|
||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
data-slot="dropdown-menu-radio-item"
|
data-slot="dropdown-menu-radio-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 whitespace-nowrap rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none overflow-hidden data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -140,7 +142,7 @@ function DropdownMenuRadioItem({
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuLabel({
|
function DropdownMenuLabel({
|
||||||
|
|
@ -148,7 +150,7 @@ function DropdownMenuLabel({
|
||||||
inset,
|
inset,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
|
|
@ -156,11 +158,11 @@ function DropdownMenuLabel({
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuSeparator({
|
function DropdownMenuSeparator({
|
||||||
|
|
@ -173,7 +175,7 @@ function DropdownMenuSeparator({
|
||||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuShortcut({
|
function DropdownMenuShortcut({
|
||||||
|
|
@ -185,17 +187,17 @@ function DropdownMenuShortcut({
|
||||||
data-slot="dropdown-menu-shortcut"
|
data-slot="dropdown-menu-shortcut"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuSub({
|
function DropdownMenuSub({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuSubTrigger({
|
function DropdownMenuSubTrigger({
|
||||||
|
|
@ -204,7 +206,7 @@ function DropdownMenuSubTrigger({
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
|
@ -212,14 +214,14 @@ function DropdownMenuSubTrigger({
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRightIcon className="ml-auto size-4" />
|
<ChevronRightIcon className="ml-auto size-4" />
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuSubContent({
|
function DropdownMenuSubContent({
|
||||||
|
|
@ -230,12 +232,12 @@ function DropdownMenuSubContent({
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
data-slot="dropdown-menu-sub-content"
|
data-slot="dropdown-menu-sub-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-[20px] border p-1 shadow-lg",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -254,4 +256,4 @@ export {
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ function InputGroupTextarea({
|
||||||
<Textarea
|
<Textarea
|
||||||
data-slot="input-group-control"
|
data-slot="input-group-control"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
|
"flex-1 resize-none rounded-none border-0 bg-transparent p-[20px] shadow-none focus-visible:ring-0 dark:bg-transparent",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,7 @@ import {
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import { DropdownSelector } from "@/components/ui/dropdown-selector";
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||||
import { CodeEditor } from "@/components/workspace/code-editor";
|
import { CodeEditor } from "@/components/workspace/code-editor";
|
||||||
import { useArtifactContent } from "@/core/artifacts/hooks";
|
import { useArtifactContent } from "@/core/artifacts/hooks";
|
||||||
|
|
@ -273,20 +266,11 @@ export function ArtifactFileDetail({
|
||||||
{isWriteFile ? (
|
{isWriteFile ? (
|
||||||
<div className=" w-full text-center overflow-hidden text-ellipsis whitespace-nowrap px-2">{truncateMiddle(getFileName(filepath), 50)}</div>
|
<div className=" w-full text-center overflow-hidden text-ellipsis whitespace-nowrap px-2">{truncateMiddle(getFileName(filepath), 50)}</div>
|
||||||
) : (
|
) : (
|
||||||
<Select value={filepath} onValueChange={select}>
|
<DropdownSelector
|
||||||
<SelectTrigger className="border-none bg-transparent! shadow-none select-none focus:outline-0 active:outline-0">
|
value={filepath}
|
||||||
<SelectValue placeholder="Select a file" />
|
options={artifactOptions}
|
||||||
</SelectTrigger>
|
onChange={select}
|
||||||
<SelectContent className="select-none">
|
/>
|
||||||
<SelectGroup>
|
|
||||||
{artifactOptions.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
)}
|
)}
|
||||||
</ArtifactTitle>
|
</ArtifactTitle>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ import { Tooltip } from "./tooltip";
|
||||||
|
|
||||||
export function InputBox({
|
export function InputBox({
|
||||||
className,
|
className,
|
||||||
|
threadId: threadIdFromProps,
|
||||||
disabled,
|
disabled,
|
||||||
autoFocus,
|
autoFocus,
|
||||||
status = "ready",
|
status = "ready",
|
||||||
|
|
@ -98,6 +99,7 @@ export function InputBox({
|
||||||
...props
|
...props
|
||||||
}: Omit<ComponentProps<typeof PromptInput>, "onSubmit"> & {
|
}: Omit<ComponentProps<typeof PromptInput>, "onSubmit"> & {
|
||||||
assistantId?: string | null;
|
assistantId?: string | null;
|
||||||
|
threadId?: string;
|
||||||
status?: ChatStatus;
|
status?: ChatStatus;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
context: Omit<
|
context: Omit<
|
||||||
|
|
@ -126,7 +128,7 @@ export function InputBox({
|
||||||
const iframeSkill = useIframeSkill();
|
const iframeSkill = useIframeSkill();
|
||||||
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const threadId = params?.thread_id;
|
const threadId = threadIdFromProps ?? params?.thread_id;
|
||||||
const { textInput } = usePromptInputController();
|
const { textInput } = usePromptInputController();
|
||||||
const attachments = usePromptInputAttachments();
|
const attachments = usePromptInputAttachments();
|
||||||
const promptRootRef = useRef<HTMLDivElement | null>(null);
|
const promptRootRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { AuroraText } from "../ui/aurora-text";
|
import { AuroraText } from "../ui/aurora-text";
|
||||||
|
|
||||||
let waved = false;
|
|
||||||
|
|
||||||
export function Welcome({
|
export function Welcome({
|
||||||
className,
|
className,
|
||||||
mode,
|
mode,
|
||||||
|
|
@ -26,9 +24,6 @@ export function Welcome({
|
||||||
}
|
}
|
||||||
return ["var(--color-foreground)"];
|
return ["var(--color-foreground)"];
|
||||||
}, [isUltra]);
|
}, [isUltra]);
|
||||||
useEffect(() => {
|
|
||||||
waved = true;
|
|
||||||
}, []);
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -40,13 +35,12 @@ export function Welcome({
|
||||||
{searchParams.get("mode") === "skill" ? (
|
{searchParams.get("mode") === "skill" ? (
|
||||||
`✨ ${t.welcome.createYourOwnSkill} ✨`
|
`✨ ${t.welcome.createYourOwnSkill} ✨`
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-2">
|
<div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
style={{ fontFamily: '"Microsoft YaHei"' }}
|
||||||
|
>
|
||||||
<AuroraText
|
<AuroraText
|
||||||
className="text-center text-[18px] leading-normal font-normal"
|
className="text-center text-[18px] leading-normal font-normal"
|
||||||
style={{
|
|
||||||
color: "var(--color-foreground, #333333)",
|
|
||||||
fontFamily: '"Microsoft YaHei"',
|
|
||||||
}}
|
|
||||||
colors={colors}
|
colors={colors}
|
||||||
>
|
>
|
||||||
{t.welcome.greeting}
|
{t.welcome.greeting}
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ export function useThreadStreamLegacy({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return thread;
|
return thread as UseStream<AgentThreadState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -474,19 +474,17 @@ export function useThreadStream({
|
||||||
} as typeof thread)
|
} as typeof thread)
|
||||||
: thread;
|
: thread;
|
||||||
|
|
||||||
return [mergedThread, sendMessage, isUploading] as const;
|
return [mergedThread as UseStream<AgentThreadState>, sendMessage, isUploading] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSubmitThread({
|
export function useSubmitThread({
|
||||||
threadId,
|
threadId,
|
||||||
thread,
|
thread,
|
||||||
threadContext,
|
threadContext,
|
||||||
isNewThread,
|
|
||||||
createNewSession,
|
createNewSession,
|
||||||
uploadTarget,
|
uploadTarget,
|
||||||
afterSubmit,
|
afterSubmit,
|
||||||
}: {
|
}: {
|
||||||
isNewThread: boolean;
|
|
||||||
createNewSession: boolean;
|
createNewSession: boolean;
|
||||||
threadId: string | null | undefined;
|
threadId: string | null | undefined;
|
||||||
thread: UseStream<AgentThreadState>;
|
thread: UseStream<AgentThreadState>;
|
||||||
|
|
@ -581,7 +579,6 @@ export function useSubmitThread({
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
thread,
|
thread,
|
||||||
isNewThread,
|
|
||||||
createNewSession,
|
createNewSession,
|
||||||
threadId,
|
threadId,
|
||||||
threadContext,
|
threadContext,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue