@kognitivedev/ui 0.2.11 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @kognitivedev/ui
2
2
 
3
+ ## 0.2.13
4
+
5
+ ### Patch Changes
6
+
7
+ - release
8
+
9
+ - Updated dependencies []:
10
+ - @kognitivedev/shared@0.2.13
11
+
12
+ ## 0.2.12
13
+
14
+ ### Patch Changes
15
+
16
+ - release
17
+
18
+ - Updated dependencies []:
19
+ - @kognitivedev/shared@0.2.12
20
+
3
21
  ## 0.2.11
4
22
 
5
23
  ### Patch Changes
package/README.md CHANGED
@@ -10,27 +10,65 @@ bun add @kognitivedev/ui
10
10
 
11
11
  **Peer dependencies:** `react`, `ai`, `@ai-sdk/react`, `zod`
12
12
 
13
- ## Quick Start
13
+ ## Quick Start: End-to-End
14
+
15
+ ### 1. Define an Agent (Server)
16
+
17
+ ```tsx
18
+ // lib/agents.ts
19
+ import { createAgent } from "@kognitivedev/agents";
20
+ import { createTool } from "@kognitivedev/tools";
21
+ import { openai } from "@ai-sdk/openai";
22
+ import { z } from "zod";
23
+
24
+ const weatherTool = createTool({
25
+ id: "weather-lookup",
26
+ description: "Get current weather for a city",
27
+ inputSchema: z.object({ city: z.string() }),
28
+ execute: async (input) => ({
29
+ city: input.city, temperature: 22, condition: "Sunny",
30
+ }),
31
+ });
32
+
33
+ export const assistant = createAgent({
34
+ name: "assistant",
35
+ instructions: "You are a helpful assistant. Use tools when needed.",
36
+ model: openai("gpt-4o-mini"),
37
+ tools: [weatherTool],
38
+ maxSteps: 5,
39
+ });
40
+ ```
41
+
42
+ ### 2. Create an API Route (Server)
43
+
44
+ ```tsx
45
+ // app/api/chat/route.ts
46
+ import { assistant } from "@/lib/agents";
47
+
48
+ export async function POST(req: Request) {
49
+ const { messages, resourceId } = await req.json();
50
+ const result = await assistant.stream({ messages, resourceId: resourceId ?? {} });
51
+ return result.toUIMessageStreamResponse();
52
+ }
53
+ ```
54
+
55
+ ### 3. Build the Chat UI (Client)
14
56
 
15
57
  ```tsx
58
+ // app/page.tsx
59
+ "use client";
16
60
  import { KognitiveUI } from "@kognitivedev/ui";
17
61
 
18
- function App() {
62
+ export default function ChatPage() {
19
63
  return (
20
- <KognitiveUI agentName="my-agent" baseUrl="http://localhost:3001">
64
+ <KognitiveUI api="/api/chat" agentName="assistant">
21
65
  <KognitiveUI.Thread />
22
66
  </KognitiveUI>
23
67
  );
24
68
  }
25
69
  ```
26
70
 
27
- Or point at a custom API route:
28
-
29
- ```tsx
30
- <KognitiveUI api="/api/chat" agentName="assistant" resourceId={{ userId: "user-1" }}>
31
- <KognitiveUI.Thread allowAttachments />
32
- </KognitiveUI>
33
- ```
71
+ That's a working chat. `api` points to your route. `agentName` is sent in the request body.
34
72
 
35
73
  ## Features
36
74
 
@@ -9,19 +9,38 @@ const composer_1 = require("../primitives/composer");
9
9
  const markdown_content_1 = require("./markdown-content");
10
10
  const tool_invocation_1 = require("./tool-invocation");
11
11
  const use_message_1 = require("../primitives/message/use-message");
12
+ // ── Inline SVG Icons (no external dependency) ──
13
+ function CopyIcon({ className }) {
14
+ return ((0, jsx_runtime_1.jsxs)("svg", { className: className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [(0, jsx_runtime_1.jsx)("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }), (0, jsx_runtime_1.jsx)("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })] }));
15
+ }
16
+ function CheckIcon({ className }) {
17
+ return ((0, jsx_runtime_1.jsx)("svg", { className: className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: (0, jsx_runtime_1.jsx)("polyline", { points: "20 6 9 17 4 12" }) }));
18
+ }
19
+ function PencilIcon({ className }) {
20
+ return ((0, jsx_runtime_1.jsxs)("svg", { className: className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [(0, jsx_runtime_1.jsx)("path", { d: "M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" }), (0, jsx_runtime_1.jsx)("path", { d: "m15 5 4 4" })] }));
21
+ }
22
+ function ThumbUpIcon({ className }) {
23
+ return ((0, jsx_runtime_1.jsxs)("svg", { className: className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [(0, jsx_runtime_1.jsx)("path", { d: "M7 10v12" }), (0, jsx_runtime_1.jsx)("path", { d: "M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z" })] }));
24
+ }
25
+ function ThumbDownIcon({ className }) {
26
+ return ((0, jsx_runtime_1.jsxs)("svg", { className: className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [(0, jsx_runtime_1.jsx)("path", { d: "M17 14V2" }), (0, jsx_runtime_1.jsx)("path", { d: "M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z" })] }));
27
+ }
28
+ const iconBtnClass = "inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800";
12
29
  function MessageInner({ className }) {
13
30
  const { message, isEditing } = (0, use_message_1.useMessage)();
14
31
  const isUser = message.role === "user";
15
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex gap-3 py-4", isUser ? "flex-row-reverse" : "flex-row", className), children: [(0, jsx_runtime_1.jsx)("div", { className: (0, cn_1.cn)("flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-sm", isUser
32
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("group relative flex gap-3 py-4", isUser ? "flex-row-reverse" : "flex-row", className), children: [(0, jsx_runtime_1.jsx)("div", { className: (0, cn_1.cn)("flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-sm font-medium", isUser
16
33
  ? "bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400"
17
- : "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400"), children: isUser ? "U" : "A" }), (0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex-1 space-y-1", isUser ? "text-right" : "text-left"), children: [isEditing ? ((0, jsx_runtime_1.jsx)(composer_1.ComposerPrimitive.EditRoot, { className: "space-y-2", children: ({ value, setValue, submit, cancel }) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("textarea", { value: value, onChange: (e) => setValue(e.target.value), className: "w-full resize-none rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-900", rows: 3 }), (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", onClick: submit, className: "rounded-lg bg-blue-600 px-3 py-1 text-xs font-medium text-white hover:bg-blue-700", children: "Save & Send" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: cancel, className: "rounded-lg border border-zinc-300 px-3 py-1 text-xs text-zinc-600 hover:bg-zinc-100 dark:border-zinc-600 dark:text-zinc-400 dark:hover:bg-zinc-800", children: "Cancel" })] })] })) })) : ((0, jsx_runtime_1.jsx)(message_1.MessagePrimitive.Content, { components: {
34
+ : "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400"), children: isUser ? "U" : "A" }), (0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex-1 min-w-0", isUser ? "text-right" : "text-left"), children: [isEditing ? ((0, jsx_runtime_1.jsx)(composer_1.ComposerPrimitive.EditRoot, { className: "space-y-2", children: ({ value, setValue, submit, cancel }) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("textarea", { value: value, onChange: (e) => setValue(e.target.value), className: "w-full resize-none rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-100", rows: 3, autoFocus: true }), (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", onClick: submit, className: "rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700", children: "Save & Send" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: cancel, className: "rounded-lg border border-zinc-300 px-3 py-1.5 text-xs text-zinc-600 hover:bg-zinc-100 dark:border-zinc-600 dark:text-zinc-400 dark:hover:bg-zinc-800", children: "Cancel" })] })] })) })) : ((0, jsx_runtime_1.jsx)(message_1.MessagePrimitive.Content, { components: {
18
35
  Text: ({ text }) => (0, jsx_runtime_1.jsx)(markdown_content_1.MarkdownContent, { text: text }),
19
36
  ToolInvocation: (props) => (0, jsx_runtime_1.jsx)(tool_invocation_1.ToolInvocation, Object.assign({}, props)),
20
- } })), !isEditing && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(message_1.MessagePrimitive.Role, { match: "user", children: (0, jsx_runtime_1.jsx)(action_bar_1.ActionBarPrimitive.Root, { className: "flex gap-1 pt-1", children: (0, jsx_runtime_1.jsx)(action_bar_1.ActionBarPrimitive.Edit, { className: "rounded px-2 py-1 text-xs text-zinc-400 hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300" }) }) }), (0, jsx_runtime_1.jsx)(message_1.MessagePrimitive.Role, { match: "assistant", children: (0, jsx_runtime_1.jsxs)(action_bar_1.ActionBarPrimitive.Root, { className: "flex gap-1 pt-1", children: [(0, jsx_runtime_1.jsx)(action_bar_1.ActionBarPrimitive.Copy, { className: "rounded px-2 py-1 text-xs text-zinc-400 hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300", children: (copied) => (copied ? "Copied!" : "Copy") }), (0, jsx_runtime_1.jsx)(action_bar_1.ActionBarPrimitive.Feedback, { className: "flex gap-1", children: ({ selected, onPositive, onNegative }) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("button", { type: "button", onClick: onPositive, className: (0, cn_1.cn)("rounded px-1.5 py-1 text-xs transition-colors", selected === "positive"
21
- ? "bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400"
22
- : "text-zinc-400 hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"), "aria-label": "Good response", children: "\uD83D\uDC4D" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: onNegative, className: (0, cn_1.cn)("rounded px-1.5 py-1 text-xs transition-colors", selected === "negative"
23
- ? "bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400"
24
- : "text-zinc-400 hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"), "aria-label": "Bad response", children: "\uD83D\uDC4E" })] })) })] }) })] }))] })] }));
37
+ } })), !isEditing && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(message_1.MessagePrimitive.Role, { match: "user", children: (0, jsx_runtime_1.jsx)(action_bar_1.ActionBarPrimitive.Root, { className: "flex gap-0.5 pt-1 opacity-0 transition-opacity group-hover:opacity-100", children: (0, jsx_runtime_1.jsx)(action_bar_1.ActionBarPrimitive.Edit, { className: (0, cn_1.cn)(iconBtnClass, "text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200"), children: (0, jsx_runtime_1.jsx)(PencilIcon, { className: "h-3.5 w-3.5" }) }) }) }), (0, jsx_runtime_1.jsx)(message_1.MessagePrimitive.Role, { match: "assistant", children: (0, jsx_runtime_1.jsxs)(action_bar_1.ActionBarPrimitive.Root, { className: "flex gap-0.5 pt-1 opacity-0 transition-opacity group-hover:opacity-100", children: [(0, jsx_runtime_1.jsx)(action_bar_1.ActionBarPrimitive.Copy, { className: (0, cn_1.cn)(iconBtnClass, "text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200"), children: (copied) => copied
38
+ ? (0, jsx_runtime_1.jsx)(CheckIcon, { className: "h-3.5 w-3.5 text-emerald-500" })
39
+ : (0, jsx_runtime_1.jsx)(CopyIcon, { className: "h-3.5 w-3.5" }) }), (0, jsx_runtime_1.jsx)(action_bar_1.ActionBarPrimitive.Feedback, { className: "flex gap-0.5", children: ({ selected, onPositive, onNegative }) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("button", { type: "button", onClick: onPositive, className: (0, cn_1.cn)(iconBtnClass, selected === "positive"
40
+ ? "text-emerald-500 bg-emerald-50 dark:bg-emerald-900/20"
41
+ : "text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200"), "aria-label": "Good response", children: (0, jsx_runtime_1.jsx)(ThumbUpIcon, { className: "h-3.5 w-3.5" }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: onNegative, className: (0, cn_1.cn)(iconBtnClass, selected === "negative"
42
+ ? "text-red-500 bg-red-50 dark:bg-red-900/20"
43
+ : "text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200"), "aria-label": "Bad response", children: (0, jsx_runtime_1.jsx)(ThumbDownIcon, { className: "h-3.5 w-3.5" }) })] })) })] }) })] }))] })] }));
25
44
  }
26
45
  function Message({ message, index, className }) {
27
46
  return ((0, jsx_runtime_1.jsx)(message_1.MessagePrimitive.Root, { message: message, index: index, children: (0, jsx_runtime_1.jsx)(MessageInner, { className: className }) }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kognitivedev/ui",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "publishConfig": {
@@ -13,7 +13,7 @@
13
13
  "test": "vitest run"
14
14
  },
15
15
  "dependencies": {
16
- "@kognitivedev/shared": "^0.2.11",
16
+ "@kognitivedev/shared": "^0.2.13",
17
17
  "clsx": "^2.1.0",
18
18
  "tailwind-merge": "^3.0.0"
19
19
  },
@@ -24,7 +24,7 @@
24
24
  "zod": ">=3.23.0"
25
25
  },
26
26
  "optionalDependencies": {
27
- "@kognitivedev/tools": "^0.2.11"
27
+ "@kognitivedev/tools": "^0.2.13"
28
28
  },
29
29
  "devDependencies": {
30
30
  "typescript": "^5.0.0",
@@ -14,6 +14,54 @@ export interface MessageProps {
14
14
  className?: string;
15
15
  }
16
16
 
17
+ // ── Inline SVG Icons (no external dependency) ──
18
+
19
+ function CopyIcon({ className }: { className?: string }) {
20
+ return (
21
+ <svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
22
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
23
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
24
+ </svg>
25
+ );
26
+ }
27
+
28
+ function CheckIcon({ className }: { className?: string }) {
29
+ return (
30
+ <svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
31
+ <polyline points="20 6 9 17 4 12" />
32
+ </svg>
33
+ );
34
+ }
35
+
36
+ function PencilIcon({ className }: { className?: string }) {
37
+ return (
38
+ <svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
39
+ <path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
40
+ <path d="m15 5 4 4" />
41
+ </svg>
42
+ );
43
+ }
44
+
45
+ function ThumbUpIcon({ className }: { className?: string }) {
46
+ return (
47
+ <svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
48
+ <path d="M7 10v12" />
49
+ <path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z" />
50
+ </svg>
51
+ );
52
+ }
53
+
54
+ function ThumbDownIcon({ className }: { className?: string }) {
55
+ return (
56
+ <svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
57
+ <path d="M17 14V2" />
58
+ <path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z" />
59
+ </svg>
60
+ );
61
+ }
62
+
63
+ const iconBtnClass = "inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800";
64
+
17
65
  function MessageInner({ className }: { className?: string }) {
18
66
  const { message, isEditing } = useMessage();
19
67
  const isUser = message.role === "user";
@@ -21,7 +69,7 @@ function MessageInner({ className }: { className?: string }) {
21
69
  return (
22
70
  <div
23
71
  className={cn(
24
- "flex gap-3 py-4",
72
+ "group relative flex gap-3 py-4",
25
73
  isUser ? "flex-row-reverse" : "flex-row",
26
74
  className,
27
75
  )}
@@ -29,7 +77,7 @@ function MessageInner({ className }: { className?: string }) {
29
77
  {/* Avatar */}
30
78
  <div
31
79
  className={cn(
32
- "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-sm",
80
+ "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-sm font-medium",
33
81
  isUser
34
82
  ? "bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400"
35
83
  : "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400",
@@ -39,8 +87,7 @@ function MessageInner({ className }: { className?: string }) {
39
87
  </div>
40
88
 
41
89
  {/* Content */}
42
- <div className={cn("flex-1 space-y-1", isUser ? "text-right" : "text-left")}>
43
- {/* Edit composer (replaces content when editing) */}
90
+ <div className={cn("flex-1 min-w-0", isUser ? "text-right" : "text-left")}>
44
91
  {isEditing ? (
45
92
  <ComposerPrimitive.EditRoot className="space-y-2">
46
93
  {({ value, setValue, submit, cancel }) => (
@@ -48,21 +95,22 @@ function MessageInner({ className }: { className?: string }) {
48
95
  <textarea
49
96
  value={value}
50
97
  onChange={(e) => setValue(e.target.value)}
51
- className="w-full resize-none rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-900"
98
+ className="w-full resize-none rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-100"
52
99
  rows={3}
100
+ autoFocus
53
101
  />
54
102
  <div className="flex gap-2">
55
103
  <button
56
104
  type="button"
57
105
  onClick={submit}
58
- className="rounded-lg bg-blue-600 px-3 py-1 text-xs font-medium text-white hover:bg-blue-700"
106
+ className="rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700"
59
107
  >
60
- Save & Send
108
+ Save &amp; Send
61
109
  </button>
62
110
  <button
63
111
  type="button"
64
112
  onClick={cancel}
65
- className="rounded-lg border border-zinc-300 px-3 py-1 text-xs text-zinc-600 hover:bg-zinc-100 dark:border-zinc-600 dark:text-zinc-400 dark:hover:bg-zinc-800"
113
+ className="rounded-lg border border-zinc-300 px-3 py-1.5 text-xs text-zinc-600 hover:bg-zinc-100 dark:border-zinc-600 dark:text-zinc-400 dark:hover:bg-zinc-800"
66
114
  >
67
115
  Cancel
68
116
  </button>
@@ -79,50 +127,57 @@ function MessageInner({ className }: { className?: string }) {
79
127
  />
80
128
  )}
81
129
 
82
- {/* Action bar */}
130
+ {/* Action bar — icon buttons, visible on hover */}
83
131
  {!isEditing && (
84
132
  <>
85
- {/* User messages: edit button */}
86
133
  <MessagePrimitive.Role match="user">
87
- <ActionBarPrimitive.Root className="flex gap-1 pt-1">
88
- <ActionBarPrimitive.Edit className="rounded px-2 py-1 text-xs text-zinc-400 hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300" />
134
+ <ActionBarPrimitive.Root className="flex gap-0.5 pt-1 opacity-0 transition-opacity group-hover:opacity-100">
135
+ <ActionBarPrimitive.Edit
136
+ className={cn(iconBtnClass, "text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200")}
137
+ >
138
+ <PencilIcon className="h-3.5 w-3.5" />
139
+ </ActionBarPrimitive.Edit>
89
140
  </ActionBarPrimitive.Root>
90
141
  </MessagePrimitive.Role>
91
142
 
92
- {/* Assistant messages: copy + feedback */}
93
143
  <MessagePrimitive.Role match="assistant">
94
- <ActionBarPrimitive.Root className="flex gap-1 pt-1">
95
- <ActionBarPrimitive.Copy className="rounded px-2 py-1 text-xs text-zinc-400 hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300">
96
- {(copied: boolean) => (copied ? "Copied!" : "Copy")}
144
+ <ActionBarPrimitive.Root className="flex gap-0.5 pt-1 opacity-0 transition-opacity group-hover:opacity-100">
145
+ <ActionBarPrimitive.Copy className={cn(iconBtnClass, "text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200")}>
146
+ {(copied: boolean) =>
147
+ copied
148
+ ? <CheckIcon className="h-3.5 w-3.5 text-emerald-500" />
149
+ : <CopyIcon className="h-3.5 w-3.5" />
150
+ }
97
151
  </ActionBarPrimitive.Copy>
98
- <ActionBarPrimitive.Feedback className="flex gap-1">
152
+
153
+ <ActionBarPrimitive.Feedback className="flex gap-0.5">
99
154
  {({ selected, onPositive, onNegative }) => (
100
155
  <>
101
156
  <button
102
157
  type="button"
103
158
  onClick={onPositive}
104
159
  className={cn(
105
- "rounded px-1.5 py-1 text-xs transition-colors",
160
+ iconBtnClass,
106
161
  selected === "positive"
107
- ? "bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400"
108
- : "text-zinc-400 hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300",
162
+ ? "text-emerald-500 bg-emerald-50 dark:bg-emerald-900/20"
163
+ : "text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200",
109
164
  )}
110
165
  aria-label="Good response"
111
166
  >
112
- &#x1F44D;
167
+ <ThumbUpIcon className="h-3.5 w-3.5" />
113
168
  </button>
114
169
  <button
115
170
  type="button"
116
171
  onClick={onNegative}
117
172
  className={cn(
118
- "rounded px-1.5 py-1 text-xs transition-colors",
173
+ iconBtnClass,
119
174
  selected === "negative"
120
- ? "bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400"
121
- : "text-zinc-400 hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300",
175
+ ? "text-red-500 bg-red-50 dark:bg-red-900/20"
176
+ : "text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200",
122
177
  )}
123
178
  aria-label="Bad response"
124
179
  >
125
- &#x1F44E;
180
+ <ThumbDownIcon className="h-3.5 w-3.5" />
126
181
  </button>
127
182
  </>
128
183
  )}