Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions app/beam-demo/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<AssistantRuntimeProvider runtime={runtime}>
<AssistantTools />
<SidebarProvider>
<ThreadListSidebar variant="floating" />
<SidebarInset className="h-svh min-w-0">
<header className="flex h-12 shrink-0 items-center gap-2 px-4">
<SidebarTrigger />
<span className="text-sm font-medium">Border Beam Demo</span>
</header>
<div className="min-h-0 flex-1">
<Thread />
</div>
</SidebarInset>
</SidebarProvider>
</AssistantRuntimeProvider>
)
}
37 changes: 3 additions & 34 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="h-svh w-full">
Expand Down
26 changes: 26 additions & 0 deletions components/assistant-ui/composer-beam.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<PulseBeam
staticColors={!isRunning}
strength={isRunning ? 1 : 0.55}
className="w-full rounded-(--composer-radius) shadow-[0_4px_16px_-8px_rgba(0,0,0,0.08),0_1px_2px_rgba(0,0,0,0.04)] transition-shadow focus-within:shadow-[0_6px_24px_-8px_rgba(0,0,0,0.12),0_1px_2px_rgba(0,0,0,0.05)] dark:shadow-none"
>
{children}
</PulseBeam>
)
}
63 changes: 63 additions & 0 deletions components/assistant-ui/pulse-beam.tsx
Original file line number Diff line number Diff line change
@@ -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 内联 <style> 的 hydration 不匹配。
// 水合完成后再切换到真实主题,服务端与客户端首帧统一按 dark 渲染。
const hydrated = useHydrated()

return (
<BorderBeam
size="pulse-inner"
colorVariant="colorful"
theme={hydrated && resolvedTheme === "light" ? "light" : "dark"}
borderRadius={borderRadius}
active={active}
staticColors={staticColors}
strength={strength}
className={className}
>
{children}
</BorderBeam>
)
}
5 changes: 4 additions & 1 deletion components/assistant-ui/thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ComposerAttachments,
UserMessageAttachments,
} from "@/components/assistant-ui/attachment";
import { ComposerBeam } from "@/components/assistant-ui/composer-beam";
import { ComposerPdfInsights } from "@/components/assistant-ui/pdf-insights";
import { DeepResearchToggle } from "@/components/assistant-ui/deep-research-toggle";
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
Expand Down Expand Up @@ -213,13 +214,15 @@ const Composer: FC = () => {
return (
<ComposerPrimitive.Root className="aui-composer-root relative flex w-full flex-col">
<ComposerPdfInsights />
<ComposerPrimitive.AttachmentDropzone render={<div data-slot="aui_composer-shell" className="border-border/60 data-[dragging=true]:border-ring focus-within:border-border dark:border-muted-foreground/15 dark:focus-within:border-muted-foreground/30 flex w-full flex-col gap-2 rounded-(--composer-radius) border bg-(--composer-bg) p-(--composer-padding) shadow-[0_4px_16px_-8px_rgba(0,0,0,0.08),0_1px_2px_rgba(0,0,0,0.04)] transition-[border-color,box-shadow] focus-within:shadow-[0_6px_24px_-8px_rgba(0,0,0,0.12),0_1px_2px_rgba(0,0,0,0.05)] data-[dragging=true]:border-dashed data-[dragging=true]:bg-[color-mix(in_oklab,var(--color-accent)_50%,var(--color-background))] dark:shadow-none" />}><ComposerAttachments /><ComposerPrimitive.Input
<ComposerBeam>
<ComposerPrimitive.AttachmentDropzone render={<div data-slot="aui_composer-shell" className="border-border/60 data-[dragging=true]:border-ring focus-within:border-border dark:border-muted-foreground/15 dark:focus-within:border-muted-foreground/30 flex w-full flex-col gap-2 rounded-(--composer-radius) border bg-(--composer-bg) p-(--composer-padding) shadow-[0_4px_16px_-8px_rgba(0,0,0,0.08),0_1px_2px_rgba(0,0,0,0.04)] transition-[border-color,box-shadow] focus-within:shadow-[0_6px_24px_-8px_rgba(0,0,0,0.12),0_1px_2px_rgba(0,0,0,0.05)] data-[dragging=true]:border-dashed data-[dragging=true]:bg-[color-mix(in_oklab,var(--color-accent)_50%,var(--color-background))] dark:shadow-none" />}><ComposerAttachments /><ComposerPrimitive.Input
placeholder="Send a message..."
className="aui-composer-input placeholder:text-muted-foreground/80 max-h-32 min-h-10 w-full resize-none bg-transparent px-2.5 py-1 text-base outline-none"
rows={1}
autoFocus
aria-label="Message input"
/><ComposerAction /></ComposerPrimitive.AttachmentDropzone>
</ComposerBeam>
</ComposerPrimitive.Root>
);
};
Expand Down
61 changes: 33 additions & 28 deletions components/assistant-ui/threadlist-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,50 @@ import {
SidebarRail,
} from "@/components/ui/sidebar";
import { ThreadList } from "@/components/assistant-ui/thread-list";
import { PulseBeam } from "@/components/assistant-ui/pulse-beam";

export function ThreadListSidebar({
...props
}: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar {...props}>
<SidebarHeader className="aui-sidebar-header mb-2 border-b">
<div className="aui-sidebar-header-content flex items-center justify-between">
{/* 光晕贴着侧边栏面板内侧呼吸;floating 变体的内面板是 rounded-2xl(16px)。
SidebarRail 靠绝对定位悬在面板边缘,放在光晕外以免被 overflow:hidden 裁掉。 */}
<PulseBeam borderRadius={16} className="flex h-full min-h-0 w-full flex-col">
<SidebarHeader className="aui-sidebar-header mb-2 border-b">
<div className="aui-sidebar-header-content flex items-center justify-between">
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" render={<a href="https://assistant-ui.com" target="_blank" rel="noopener noreferrer" />}><div className="aui-sidebar-header-icon-wrapper bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<MessagesSquare className="aui-sidebar-header-icon size-4" />
</div><div className="aui-sidebar-header-heading me-6 flex flex-col gap-0.5 leading-none">
<span className="aui-sidebar-header-title font-semibold">
assistant-ui
</span>
</div></SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</div>
</SidebarHeader>
<SidebarContent className="aui-sidebar-content px-2">
<ThreadList />
</SidebarContent>
<SidebarFooter className="aui-sidebar-footer border-t">
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" render={<a href="https://assistant-ui.com" target="_blank" rel="noopener noreferrer" />}><div className="aui-sidebar-header-icon-wrapper bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<MessagesSquare className="aui-sidebar-header-icon size-4" />
</div><div className="aui-sidebar-header-heading me-6 flex flex-col gap-0.5 leading-none">
<span className="aui-sidebar-header-title font-semibold">
assistant-ui
</span>
</div></SidebarMenuButton>
<SidebarMenuButton size="lg" render={<a href="https://github.com/assistant-ui/assistant-ui" target="_blank" rel="noopener noreferrer" />}><div className="aui-sidebar-footer-icon-wrapper bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<GitHubIcon className="aui-sidebar-footer-icon size-4" />
</div><div className="aui-sidebar-footer-heading flex flex-col gap-0.5 leading-none">
<span className="aui-sidebar-footer-title font-semibold">
GitHub
</span>
<span>View Source</span>
</div></SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</div>
</SidebarHeader>
<SidebarContent className="aui-sidebar-content px-2">
<ThreadList />
</SidebarContent>
</SidebarFooter>
</PulseBeam>
<SidebarRail />
<SidebarFooter className="aui-sidebar-footer border-t">
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" render={<a href="https://github.com/assistant-ui/assistant-ui" target="_blank" rel="noopener noreferrer" />}><div className="aui-sidebar-footer-icon-wrapper bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<GitHubIcon className="aui-sidebar-footer-icon size-4" />
</div><div className="aui-sidebar-footer-heading flex flex-col gap-0.5 leading-none">
<span className="aui-sidebar-footer-title font-semibold">
GitHub
</span>
<span>View Source</span>
</div></SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
);
}
3 changes: 3 additions & 0 deletions components/examples/base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ComposerAttachments,
UserMessageAttachments,
} from "@/components/assistant-ui/attachment";
import { ComposerBeam } from "@/components/assistant-ui/composer-beam";
import { ComposerPdfInsights } from "@/components/assistant-ui/pdf-insights";
import { DeepResearchToggle } from "@/components/assistant-ui/deep-research-toggle";
import {
Expand Down Expand Up @@ -570,6 +571,7 @@ const Composer: FC = () => {
return (
<ComposerPrimitive.Unstable_TriggerPopoverRoot>
<ComposerPrimitive.Root className="aui-composer-root relative flex w-full flex-col">
<ComposerBeam>
<ComposerPrimitive.AttachmentDropzone asChild>
<div
data-slot="aui_composer-shell"
Expand All @@ -586,6 +588,7 @@ const Composer: FC = () => {
<ComposerAction />
</div>
</ComposerPrimitive.AttachmentDropzone>
</ComposerBeam>
<ComposerTriggerPopover char="@" {...mention} />
<ComposerTriggerPopover
char="/"
Expand Down
55 changes: 55 additions & 0 deletions lib/chat/use-thread-chat-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client"

import { useMemo } from "react"
import { useRemoteThreadListRuntime } from "@assistant-ui/react"
import {
AssistantChatTransport,
useChatRuntime,
} from "@assistant-ui/react-ai-sdk"
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 },
})
}

/**
* 本项目的完整聊天 runtime:AI SDK transport + Postgres 线程列表/历史持久化。
* 供 app/page.tsx 与各 demo 页面共用,传给 AssistantRuntimeProvider。
*/
export function useThreadChatRuntime() {
return useRemoteThreadListRuntime({
runtimeHook: useMyChatRuntime,
adapter: postgresThreadListAdapter,
})
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"ai": "^7.0.14",
"assistant-stream": "^0.3.25",
"beautiful-mermaid": "^1.1.3",
"border-beam": "^1.3.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
Expand Down
Loading
Loading