Skip to content

Commit 7aa133f

Browse files
committed
simple chatui
Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
1 parent ab8a02a commit 7aa133f

File tree

17 files changed

+808
-0
lines changed

17 files changed

+808
-0
lines changed

integrations/chat-ui/package.json

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{
2+
"name": "@databricks/chat-ui",
3+
"version": "0.1.0",
4+
"description": "React chat UI components for Databricks AI agents",
5+
"type": "module",
6+
"main": "./dist/index.cjs",
7+
"module": "./dist/index.mjs",
8+
"types": "./dist/index.d.cts",
9+
"exports": {
10+
".": {
11+
"import": {
12+
"types": "./dist/index.d.mts",
13+
"default": "./dist/index.mjs"
14+
},
15+
"require": {
16+
"types": "./dist/index.d.cts",
17+
"default": "./dist/index.cjs"
18+
}
19+
},
20+
"./styles.css": "./dist/styles.css"
21+
},
22+
"files": [
23+
"dist",
24+
"README.md",
25+
"LICENSE",
26+
"NOTICE"
27+
],
28+
"scripts": {
29+
"build": "tsdown && tailwindcss -i src/styles.css -o dist/styles.css --minify",
30+
"dev": "tsdown --watch",
31+
"clean": "rm -rf dist",
32+
"test": "vitest run",
33+
"test:watch": "vitest",
34+
"typecheck": "tsc --noEmit",
35+
"format": "prettier --write 'src/**/*.{ts,tsx}'",
36+
"format:check": "prettier --check 'src/**/*.{ts,tsx}'"
37+
},
38+
"engines": {
39+
"node": ">=18.0.0"
40+
},
41+
"peerDependencies": {
42+
"react": "^18.0.0 || ^19.0.0",
43+
"react-dom": "^18.0.0 || ^19.0.0"
44+
},
45+
"dependencies": {
46+
"@radix-ui/react-slot": "^1.2.4",
47+
"class-variance-authority": "^0.7.1",
48+
"clsx": "^2.1.1",
49+
"tailwind-merge": "^3.4.0"
50+
},
51+
"devDependencies": {
52+
"@types/node": "^22.0.0",
53+
"@types/react": "^19.0.0",
54+
"@types/react-dom": "^19.0.0",
55+
"prettier": "^3.0.0",
56+
"tailwindcss": "^3.4.0",
57+
"react": "^19.0.0",
58+
"react-dom": "^19.0.0",
59+
"tsdown": "^0.9.0",
60+
"typescript": "^5.4.0",
61+
"vitest": "^4.0.18"
62+
},
63+
"author": {
64+
"name": "Databricks",
65+
"email": "agent-feedback@databricks.com"
66+
},
67+
"repository": {
68+
"type": "git",
69+
"url": "https://github.com/databricks/databricks-ai-bridge.git",
70+
"directory": "integrations/chat-ui"
71+
},
72+
"homepage": "https://github.com/databricks/databricks-ai-bridge/tree/main/integrations/chat-ui",
73+
"bugs": {
74+
"url": "https://github.com/databricks/databricks-ai-bridge/issues"
75+
},
76+
"keywords": [
77+
"databricks",
78+
"chat",
79+
"agent",
80+
"react",
81+
"ui",
82+
"components"
83+
],
84+
"license": "Databricks License"
85+
}

integrations/chat-ui/src/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export {
2+
SimpleAgentChat,
3+
AgentChatMessage,
4+
AgentChatPart,
5+
useAgentChat,
6+
serializeForApi,
7+
tryFormatJson,
8+
} from "./simple-agent-chat";
9+
export type {
10+
SimpleAgentChatProps,
11+
ChatMessage,
12+
AssistantPart,
13+
TextPart,
14+
FunctionCallPart,
15+
FunctionCallOutputPart,
16+
UseAgentChatOptions,
17+
UseAgentChatReturn,
18+
} from "./simple-agent-chat";
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { AgentChatPart } from "./agent-chat-part";
2+
import type { ChatMessage } from "./types";
3+
4+
interface AgentChatMessageProps {
5+
message: ChatMessage;
6+
isLast?: boolean;
7+
isStreaming?: boolean;
8+
}
9+
10+
/** Renders a single chat message bubble (user or assistant with parts). */
11+
export function AgentChatMessage({
12+
message,
13+
isLast = false,
14+
isStreaming = false,
15+
}: AgentChatMessageProps) {
16+
if (message.role === "user") {
17+
return (
18+
<div className="text-right">
19+
<span className="text-xs font-medium text-muted-foreground block mb-1">
20+
You
21+
</span>
22+
<div className="inline-block rounded-lg bg-primary text-primary-foreground px-3 py-2 text-sm max-w-[85%]">
23+
{message.content}
24+
</div>
25+
</div>
26+
);
27+
}
28+
29+
return (
30+
<div className="text-left text-foreground space-y-2">
31+
<span className="text-xs font-medium text-muted-foreground block mb-1">
32+
Agent
33+
</span>
34+
<div className="space-y-2 max-w-[85%]">
35+
{message.parts.map((part, j) => (
36+
<AgentChatPart
37+
key={`part-${part.type}-${j}`}
38+
part={part}
39+
showCursor={isLast && j === message.parts.length - 1 && isStreaming}
40+
/>
41+
))}
42+
</div>
43+
</div>
44+
);
45+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { AssistantPart } from "./types";
2+
import { tryFormatJson } from "./utils";
3+
4+
interface AgentChatPartProps {
5+
part: AssistantPart;
6+
showCursor?: boolean;
7+
}
8+
9+
/** Renders a single assistant part: text, function_call, or function_call_output. */
10+
export function AgentChatPart({
11+
part,
12+
showCursor = false,
13+
}: AgentChatPartProps) {
14+
if (part.type === "text") {
15+
return (
16+
<div className="rounded-lg bg-muted px-3 py-2 text-sm whitespace-pre-wrap">
17+
{part.content}
18+
{showCursor && <span className="animate-pulse">|</span>}
19+
</div>
20+
);
21+
}
22+
23+
if (part.type === "function_call") {
24+
return (
25+
<div className="rounded-lg border border-amber-500/50 bg-amber-500/10 px-3 py-2 text-sm">
26+
<div className="font-medium text-amber-700 dark:text-amber-400 mb-1">
27+
Tool: {part.name}
28+
</div>
29+
<pre className="text-xs overflow-x-auto text-muted-foreground font-mono whitespace-pre-wrap break-words">
30+
{tryFormatJson(part.arguments)}
31+
</pre>
32+
</div>
33+
);
34+
}
35+
36+
return (
37+
<div className="rounded-lg border border-emerald-500/50 bg-emerald-500/10 px-3 py-2 text-sm">
38+
<div className="font-medium text-emerald-700 dark:text-emerald-400 mb-1">
39+
Result
40+
</div>
41+
<pre className="text-xs overflow-x-auto text-muted-foreground font-mono whitespace-pre-wrap break-words">
42+
{tryFormatJson(part.output)}
43+
</pre>
44+
</div>
45+
);
46+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export { SimpleAgentChat } from "./simple-agent-chat";
2+
export { AgentChatMessage } from "./agent-chat-message";
3+
export { AgentChatPart } from "./agent-chat-part";
4+
export type {
5+
SimpleAgentChatProps,
6+
ChatMessage,
7+
AssistantPart,
8+
TextPart,
9+
FunctionCallPart,
10+
FunctionCallOutputPart,
11+
UseAgentChatOptions,
12+
UseAgentChatReturn,
13+
} from "./types";
14+
export { useAgentChat } from "./use-agent-chat";
15+
export { serializeForApi, tryFormatJson } from "./utils";
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type * as React from "react";
2+
import { Slot } from "@radix-ui/react-slot";
3+
import { cva, type VariantProps } from "class-variance-authority";
4+
import { cn } from "./cn";
5+
6+
const buttonVariants = cva(
7+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:cursor-pointer",
8+
{
9+
variants: {
10+
variant: {
11+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
12+
destructive:
13+
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
14+
outline:
15+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
16+
secondary:
17+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
18+
ghost:
19+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
20+
link: "text-primary underline-offset-4 hover:underline",
21+
},
22+
size: {
23+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
24+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
25+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
26+
icon: "size-9",
27+
"icon-sm": "size-8",
28+
"icon-lg": "size-10",
29+
},
30+
},
31+
defaultVariants: {
32+
variant: "default",
33+
size: "default",
34+
},
35+
},
36+
);
37+
38+
export function Button({
39+
className,
40+
variant,
41+
size,
42+
asChild = false,
43+
...props
44+
}: React.ComponentProps<"button"> &
45+
VariantProps<typeof buttonVariants> & {
46+
asChild?: boolean;
47+
}) {
48+
const Comp = asChild ? Slot : "button";
49+
50+
return (
51+
<Comp
52+
data-slot="button"
53+
className={cn(buttonVariants({ variant, size, className }))}
54+
{...props}
55+
/>
56+
);
57+
}
58+
59+
export { buttonVariants };
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type * as React from "react";
2+
import { cn } from "./cn";
3+
4+
export function Card({ className, ...props }: React.ComponentProps<"div">) {
5+
return (
6+
<div
7+
data-slot="card"
8+
className={cn(
9+
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border border-border py-6 shadow-sm",
10+
className,
11+
)}
12+
{...props}
13+
/>
14+
);
15+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { type ClassValue, clsx } from "clsx";
2+
import { twMerge } from "tailwind-merge";
3+
4+
export function cn(...inputs: ClassValue[]) {
5+
return twMerge(clsx(inputs));
6+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { useEffect, useRef } from "react";
2+
import { cn } from "./primitives/cn";
3+
import { Button } from "./primitives/button";
4+
import { Card } from "./primitives/card";
5+
import { AgentChatMessage } from "./agent-chat-message";
6+
import type { SimpleAgentChatProps, ChatMessage } from "./types";
7+
import { useAgentChat } from "./use-agent-chat";
8+
9+
/** Agent chat UI: message list + input, wired to POST /invocations SSE streaming. */
10+
export function SimpleAgentChat({
11+
invokeUrl = "/invocations",
12+
placeholder = "Type a message...",
13+
emptyMessage = "Send a message to start.",
14+
className,
15+
}: SimpleAgentChatProps) {
16+
const scrollRef = useRef<HTMLDivElement>(null);
17+
const {
18+
displayMessages,
19+
loading,
20+
input,
21+
setInput,
22+
handleSubmit,
23+
isStreamingText,
24+
} = useAgentChat({ invokeUrl });
25+
26+
const contentLength = displayMessages.length;
27+
// biome-ignore lint/correctness/useExhaustiveDependencies: deps used as triggers for auto-scroll
28+
useEffect(() => {
29+
scrollRef.current?.scrollTo({
30+
top: scrollRef.current.scrollHeight,
31+
behavior: "smooth",
32+
});
33+
}, [contentLength, isStreamingText]);
34+
35+
return (
36+
<div data-chat-ui="" className={cn("flex flex-col min-h-0", className)}>
37+
<Card className="flex-1 flex flex-col min-h-0 p-4">
38+
<div ref={scrollRef} className="flex-1 overflow-y-auto space-y-4 mb-4">
39+
{displayMessages.length === 0 && (
40+
<p className="text-muted-foreground text-sm">{emptyMessage}</p>
41+
)}
42+
{displayMessages.map((msg, i) => (
43+
<MessageItem
44+
key={`msg-${i}-${msg.role}`}
45+
message={msg}
46+
isLast={i === displayMessages.length - 1}
47+
isStreaming={isStreamingText}
48+
/>
49+
))}
50+
</div>
51+
52+
<form onSubmit={handleSubmit} className="flex gap-2">
53+
<input
54+
type="text"
55+
value={input}
56+
onChange={(e) => setInput(e.target.value)}
57+
placeholder={placeholder}
58+
className="flex-1 rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
59+
disabled={loading}
60+
/>
61+
<Button type="submit" disabled={loading || !input.trim()}>
62+
{loading ? "..." : "Send"}
63+
</Button>
64+
</form>
65+
</Card>
66+
</div>
67+
);
68+
}
69+
70+
function MessageItem({
71+
message,
72+
isLast,
73+
isStreaming,
74+
}: {
75+
message: ChatMessage;
76+
isLast: boolean;
77+
isStreaming: boolean;
78+
}) {
79+
return (
80+
<AgentChatMessage
81+
message={message}
82+
isLast={isLast}
83+
isStreaming={isStreaming}
84+
/>
85+
);
86+
}

0 commit comments

Comments
 (0)