Skip to content

Commit 9a853a1

Browse files
mschristensenclaude
andcommitted
demo: add split-pane mode with per-view send
Adds a "Split Pane" toggle to the header that renders two side-by-side chat panes sharing the same transport but with independent views. Each pane has its own InputBar and MessageQueue, and uses view.send() so messages chain off that pane's selected branch. New ChatPane component encapsulates a labeled pane with its own message list, input bar, and queue. Uses ViewHandle.send/regenerate/edit directly — no manual parent workaround needed since View owns the write path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 93ffa91 commit 9a853a1

3 files changed

Lines changed: 107 additions & 26 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use client';
2+
3+
import type { UIMessage, UIMessageChunk } from 'ai';
4+
import type { ViewHandle } from '@ably/ai-transport/react';
5+
import type { ClientTransport } from '@ably/ai-transport';
6+
import { MessageList } from './message-list';
7+
import { InputBar } from './input-bar';
8+
import { MessageQueue } from './message-queue';
9+
import { useMessageQueue } from '../hooks/use-message-queue';
10+
import { userMessage } from '../helpers';
11+
12+
interface ChatPaneProps {
13+
label: string;
14+
transport: ClientTransport<UIMessageChunk, UIMessage>;
15+
view: ViewHandle<UIMessageChunk, UIMessage>;
16+
activeTurns: Map<string, Set<string>>;
17+
clientId: string | undefined;
18+
}
19+
20+
export function ChatPane({ label, transport, view, activeTurns, clientId }: ChatPaneProps) {
21+
const queue = useMessageQueue(transport, view.send);
22+
23+
return (
24+
<div className="flex flex-1 flex-col min-w-0">
25+
<div className="flex items-center gap-2 border-b border-zinc-800 px-3 py-1.5">
26+
<span className="text-[10px] font-semibold uppercase tracking-wider text-zinc-500">{label}</span>
27+
</div>
28+
<MessageList
29+
view={view}
30+
onRegenerate={(id) => view.regenerate(id)}
31+
onEdit={(id, text) => view.edit(id, [userMessage(text)])}
32+
/>
33+
<MessageQueue queue={queue} />
34+
<InputBar
35+
transport={transport}
36+
send={view.send}
37+
activeTurns={activeTurns}
38+
clientId={clientId}
39+
queue={queue}
40+
/>
41+
</div>
42+
);
43+
}

demo/vercel/react/use-client-transport/src/app/components/chat.tsx

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
'use client';
22

3+
import { useState, useMemo } from 'react';
34
import { useChannel } from 'ably/react';
4-
import {
5-
useClientTransport,
6-
useSend,
7-
useRegenerate,
8-
useEdit,
9-
useActiveTurns,
10-
useView,
11-
useAblyMessages,
12-
} from '@ably/ai-transport/react';
5+
import { useClientTransport, useActiveTurns, useView, useAblyMessages } from '@ably/ai-transport/react';
136
import { UIMessageCodec } from '@ably/ai-transport/vercel';
147

158
import { userMessage } from '../helpers';
@@ -19,6 +12,7 @@ import { MessageList } from './message-list';
1912
import { MessageQueue } from './message-queue';
2013
import { InputBar } from './input-bar';
2114
import { DebugPane } from './debug-pane';
15+
import { ChatPane } from './chat-pane';
2216

2317
interface ChatProps {
2418
chatId: string;
@@ -28,6 +22,7 @@ interface ChatProps {
2822

2923
export function Chat({ chatId, clientId, historyLimit }: ChatProps) {
3024
const { channel } = useChannel({ channelName: chatId });
25+
const [split, setSplit] = useState(false);
3126

3227
const transport = useClientTransport({
3328
channel,
@@ -37,30 +32,56 @@ export function Chat({ chatId, clientId, historyLimit }: ChatProps) {
3732
});
3833

3934
const view = useView(transport, { limit: historyLimit ?? 30 });
40-
const send = useSend(transport);
41-
const regenerate = useRegenerate(transport);
42-
const edit = useEdit(transport);
35+
const secondView = useMemo(() => transport.createView(), [transport]);
36+
const view2 = useView(secondView, { limit: historyLimit ?? 30 });
37+
4338
const activeTurns = useActiveTurns(transport);
4439
const ablyMessages = useAblyMessages(transport);
45-
const queue = useMessageQueue(transport, send);
40+
const queue = useMessageQueue(transport, view.send);
4641

4742
return (
4843
<div className="flex h-dvh">
4944
<div className="flex flex-1 flex-col">
50-
<Header clientId={clientId} />
51-
<MessageList
52-
view={view}
53-
onRegenerate={(id) => regenerate(id)}
54-
onEdit={(id, text) => edit(id, [userMessage(text)])}
55-
/>
56-
<MessageQueue queue={queue} />
57-
<InputBar
58-
transport={transport}
59-
send={send}
60-
activeTurns={activeTurns}
45+
<Header
6146
clientId={clientId}
62-
queue={queue}
47+
split={split}
48+
onToggleSplit={() => setSplit((s) => !s)}
6349
/>
50+
{split ? (
51+
<div className="flex flex-1 min-h-0">
52+
<ChatPane
53+
label="View A"
54+
transport={transport}
55+
view={view}
56+
activeTurns={activeTurns}
57+
clientId={clientId}
58+
/>
59+
<div className="w-px bg-zinc-800" />
60+
<ChatPane
61+
label="View B"
62+
transport={transport}
63+
view={view2}
64+
activeTurns={activeTurns}
65+
clientId={clientId}
66+
/>
67+
</div>
68+
) : (
69+
<>
70+
<MessageList
71+
view={view}
72+
onRegenerate={(id) => view.regenerate(id)}
73+
onEdit={(id, text) => view.edit(id, [userMessage(text)])}
74+
/>
75+
<MessageQueue queue={queue} />
76+
<InputBar
77+
transport={transport}
78+
send={view.send}
79+
activeTurns={activeTurns}
80+
clientId={clientId}
81+
queue={queue}
82+
/>
83+
</>
84+
)}
6485
</div>
6586
<DebugPane
6687
messages={view.nodes.map((n) => n.message)}

demo/vercel/react/use-client-transport/src/app/components/header.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
11
'use client';
22

3-
export function Header({ clientId }: { clientId?: string }) {
3+
interface HeaderProps {
4+
clientId?: string;
5+
split?: boolean;
6+
onToggleSplit?: () => void;
7+
}
8+
9+
export function Header({ clientId, split, onToggleSplit }: HeaderProps) {
410
return (
511
<header className="flex items-center gap-2 border-b border-zinc-800 px-4 py-3">
612
<div className="h-2 w-2 rounded-full bg-emerald-500" />
713
<h1 className="text-sm font-medium text-zinc-300">Ably AI — Client Transport Demo</h1>
14+
{onToggleSplit && (
15+
<button
16+
type="button"
17+
onClick={onToggleSplit}
18+
className={`ml-4 rounded px-2 py-1 text-xs font-medium transition-colors ${
19+
split ? 'bg-zinc-600 text-zinc-100' : 'bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-300'
20+
}`}
21+
>
22+
{split ? 'Single Pane' : 'Split Pane'}
23+
</button>
24+
)}
825
{clientId && <span className="ml-auto text-xs text-zinc-600 font-mono">{clientId}</span>}
926
</header>
1027
);

0 commit comments

Comments
 (0)