diff --git a/app/beam-demo/page.tsx b/app/beam-demo/page.tsx new file mode 100644 index 00000000..555c10b1 --- /dev/null +++ b/app/beam-demo/page.tsx @@ -0,0 +1,39 @@ +"use client" + +import { AssistantRuntimeProvider } from "@assistant-ui/react" +import { Thread } from "@/components/assistant-ui/thread" +import { ThreadListSidebar } from "@/components/assistant-ui/threadlist-sidebar" +import { AssistantTools } from "@/components/assistant-ui/tools" +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar" +import { useThreadChatRuntime } from "@/lib/chat/use-thread-chat-runtime" + +/** + * border-beam 风格 demo:assistant-ui 的 ThreadListSidebar(floating 变体) + * 与 Thread 组合,侧边栏面板和 composer 都带 pulse-inner 呼吸光晕, + * 聊天走与首页相同的完整 runtime(MiniMax 流式 + Postgres 持久化)。 + */ +export default function BeamDemoPage() { + const runtime = useThreadChatRuntime() + + return ( + + + + + +
+ + Border Beam Demo +
+
+ +
+
+
+
+ ) +} diff --git a/app/page.tsx b/app/page.tsx index d32c3676..4cd18b2d 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,43 +1,12 @@ "use client" -import { useMemo } from "react" -import { AssistantRuntimeProvider, useRemoteThreadListRuntime } from "@assistant-ui/react" -import { AssistantChatTransport, useChatRuntime } from "@assistant-ui/react-ai-sdk" +import { AssistantRuntimeProvider } from "@assistant-ui/react" import { Base } from "@/components/examples/base" import { AssistantTools } from "@/components/assistant-ui/tools" -import { postgresThreadListAdapter } from "@/lib/chat/thread-list-adapter" -import { usePostgresThreadHistoryAdapter } from "@/lib/chat/use-thread-history-adapter" -import { r2AttachmentAdapter } from "@/lib/chat/attachment-adapter" -import { useResearchMode } from "@/lib/chat/research-mode" - -function useMyChatRuntime() { - const history = usePostgresThreadHistoryAdapter() - // 把「深度研究」开关状态随每条消息发给 chat route。用 getState() 而非闭包快照, - // 保证读到发送时的最新开关值。 - const transport = useMemo( - () => - new AssistantChatTransport({ - prepareSendMessagesRequest: ({ id, messages, trigger, messageId, body }) => ({ - body: { - ...body, - id, - messages, - trigger, - messageId, - deepResearch: useResearchMode.getState().enabled, - }, - }), - }), - [], - ) - return useChatRuntime({ transport, adapters: { history, attachments: r2AttachmentAdapter } }) -} +import { useThreadChatRuntime } from "@/lib/chat/use-thread-chat-runtime" export default function Page() { - const runtime = useRemoteThreadListRuntime({ - runtimeHook: useMyChatRuntime, - adapter: postgresThreadListAdapter, - }) + const runtime = useThreadChatRuntime() return (
diff --git a/components/assistant-ui/composer-beam.tsx b/components/assistant-ui/composer-beam.tsx new file mode 100644 index 00000000..b1d63551 --- /dev/null +++ b/components/assistant-ui/composer-beam.tsx @@ -0,0 +1,26 @@ +"use client" + +import type { FC, ReactNode } from "react" +import { useAuiState } from "@assistant-ui/react" +import { PulseBeam } from "@/components/assistant-ui/pulse-beam" + +/** + * composer 外壳专用的 pulse-inner 呼吸光晕,绑定 AI 回复状态的混合方案: + * 空闲时色相定格 + 低强度微光(仍轻微呼吸),回复中恢复全彩流动与全亮度, + * 作为「正在生成」的运行状态反馈。 + * wrapper 自带 overflow:hidden 会裁掉外壳的投影, + * 所以把浅色模式的投影从外壳复制到 wrapper 上。 + */ +export const ComposerBeam: FC<{ children: ReactNode }> = ({ children }) => { + const isRunning = useAuiState((s) => s.thread.isRunning) + + return ( + + {children} + + ) +} diff --git a/components/assistant-ui/pulse-beam.tsx b/components/assistant-ui/pulse-beam.tsx new file mode 100644 index 00000000..ebf98b5f --- /dev/null +++ b/components/assistant-ui/pulse-beam.tsx @@ -0,0 +1,63 @@ +"use client" + +import { useSyncExternalStore, type FC, type ReactNode } from "react" +import { BorderBeam } from "border-beam" +import { useTheme } from "next-themes" + +// 水合安全的挂载检测:服务端快照为 false,水合完成后 React 以客户端快照 true 重渲染。 +const noopSubscribe = () => () => {} +function useHydrated() { + return useSyncExternalStore( + noopSubscribe, + () => true, + () => false + ) +} + +/** + * border-beam pulse-inner 呼吸光晕的通用包装, + * 复刻 https://beam.jakubantalik.com/pulse 的光影效果: + * 彩色光晕贴着元素内侧四周弥散、缓慢呼吸(裁剪在边框以内)。 + * 跟随 next-themes 的明暗主题;圆角默认自动探测子元素, + * 子元素圆角探测不到时(如首子元素是无圆角的内部布局)可显式传 borderRadius。 + * active 控制光晕开关(带淡入/淡出过渡),用于绑定 AI 回复中等运行状态。 + * staticColors 冻结色相流转(呼吸仍在),strength 控制整体亮度(0-1), + * 两者组合可实现「空闲微光、运行全彩流动」的混合状态。 + */ +export const PulseBeam: FC<{ + children: ReactNode + className?: string + borderRadius?: number + active?: boolean + staticColors?: boolean + strength?: number +}> = ({ + children, + className, + borderRadius, + active = true, + staticColors, + strength, +}) => { + const { resolvedTheme } = useTheme() + + // SSR 时 resolvedTheme 为 undefined,客户端首帧可能已解析出 light, + // 直接使用会导致 BorderBeam 内联