@castlekit/castle 0.1.5 → 0.3.0
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/drizzle.config.ts +7 -0
- package/next.config.ts +1 -0
- package/package.json +25 -4
- package/src/app/api/avatars/[id]/route.ts +122 -25
- package/src/app/api/openclaw/agents/[id]/avatar/route.ts +216 -0
- package/src/app/api/openclaw/agents/route.ts +77 -41
- package/src/app/api/openclaw/agents/status/route.ts +55 -0
- package/src/app/api/openclaw/chat/attachments/route.ts +230 -0
- package/src/app/api/openclaw/chat/channels/route.ts +214 -0
- package/src/app/api/openclaw/chat/route.ts +272 -0
- package/src/app/api/openclaw/chat/search/route.ts +149 -0
- package/src/app/api/openclaw/chat/storage/route.ts +75 -0
- package/src/app/api/openclaw/config/route.ts +45 -4
- package/src/app/api/openclaw/events/route.ts +31 -2
- package/src/app/api/openclaw/logs/route.ts +20 -5
- package/src/app/api/openclaw/restart/route.ts +12 -4
- package/src/app/api/openclaw/session/status/route.ts +42 -0
- package/src/app/api/settings/avatar/route.ts +190 -0
- package/src/app/api/settings/route.ts +88 -0
- package/src/app/chat/[channelId]/error-boundary.tsx +64 -0
- package/src/app/chat/[channelId]/page.tsx +305 -0
- package/src/app/chat/layout.tsx +96 -0
- package/src/app/chat/page.tsx +52 -0
- package/src/app/globals.css +89 -2
- package/src/app/layout.tsx +7 -1
- package/src/app/page.tsx +147 -28
- package/src/app/settings/page.tsx +300 -0
- package/src/cli/onboarding.ts +202 -37
- package/src/components/chat/agent-mention-popup.tsx +89 -0
- package/src/components/chat/archived-channels.tsx +190 -0
- package/src/components/chat/channel-list.tsx +140 -0
- package/src/components/chat/chat-input.tsx +310 -0
- package/src/components/chat/create-channel-dialog.tsx +171 -0
- package/src/components/chat/markdown-content.tsx +205 -0
- package/src/components/chat/message-bubble.tsx +152 -0
- package/src/components/chat/message-list.tsx +508 -0
- package/src/components/chat/message-queue.tsx +68 -0
- package/src/components/chat/session-divider.tsx +61 -0
- package/src/components/chat/session-stats-panel.tsx +139 -0
- package/src/components/chat/storage-indicator.tsx +76 -0
- package/src/components/layout/sidebar.tsx +126 -45
- package/src/components/layout/user-menu.tsx +29 -4
- package/src/components/providers/presence-provider.tsx +8 -0
- package/src/components/providers/search-provider.tsx +81 -0
- package/src/components/search/search-dialog.tsx +269 -0
- package/src/components/ui/avatar.tsx +11 -9
- package/src/components/ui/dialog.tsx +10 -4
- package/src/components/ui/tooltip.tsx +25 -8
- package/src/components/ui/twemoji-text.tsx +37 -0
- package/src/lib/api-security.ts +188 -0
- package/src/lib/config.ts +36 -4
- package/src/lib/date-utils.ts +79 -0
- package/src/lib/db/__tests__/queries.test.ts +318 -0
- package/src/lib/db/index.ts +642 -0
- package/src/lib/db/queries.ts +1017 -0
- package/src/lib/db/schema.ts +160 -0
- package/src/lib/device-identity.ts +303 -0
- package/src/lib/gateway-connection.ts +273 -36
- package/src/lib/hooks/use-agent-status.ts +251 -0
- package/src/lib/hooks/use-chat.ts +775 -0
- package/src/lib/hooks/use-openclaw.ts +105 -70
- package/src/lib/hooks/use-search.ts +113 -0
- package/src/lib/hooks/use-session-stats.ts +57 -0
- package/src/lib/hooks/use-user-settings.ts +46 -0
- package/src/lib/types/chat.ts +186 -0
- package/src/lib/types/search.ts +60 -0
- package/src/middleware.ts +52 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useCallback } from "react";
|
|
4
|
+
import ReactMarkdown from "react-markdown";
|
|
5
|
+
import remarkGfm from "remark-gfm";
|
|
6
|
+
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
7
|
+
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
|
8
|
+
import { Copy, Check } from "lucide-react";
|
|
9
|
+
import { cn } from "@/lib/utils";
|
|
10
|
+
|
|
11
|
+
interface MarkdownContentProps {
|
|
12
|
+
content: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function CopyButton({ text }: { text: string }) {
|
|
17
|
+
const [copied, setCopied] = useState(false);
|
|
18
|
+
|
|
19
|
+
const handleCopy = useCallback(async () => {
|
|
20
|
+
try {
|
|
21
|
+
await navigator.clipboard.writeText(text);
|
|
22
|
+
setCopied(true);
|
|
23
|
+
setTimeout(() => setCopied(false), 2000);
|
|
24
|
+
} catch {
|
|
25
|
+
// Fallback for non-HTTPS
|
|
26
|
+
const textarea = document.createElement("textarea");
|
|
27
|
+
textarea.value = text;
|
|
28
|
+
document.body.appendChild(textarea);
|
|
29
|
+
textarea.select();
|
|
30
|
+
document.execCommand("copy");
|
|
31
|
+
document.body.removeChild(textarea);
|
|
32
|
+
setCopied(true);
|
|
33
|
+
setTimeout(() => setCopied(false), 2000);
|
|
34
|
+
}
|
|
35
|
+
}, [text]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
onClick={handleCopy}
|
|
41
|
+
className="absolute top-2 right-2 p-1.5 rounded-md bg-white/10 hover:bg-white/20 transition-colors text-white/60 hover:text-white/90"
|
|
42
|
+
title={copied ? "Copied!" : "Copy code"}
|
|
43
|
+
>
|
|
44
|
+
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
|
|
45
|
+
</button>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Renders markdown content with syntax highlighting for code blocks.
|
|
51
|
+
* HTML rendering is disabled for XSS safety.
|
|
52
|
+
*/
|
|
53
|
+
export function MarkdownContent({ content, className }: MarkdownContentProps) {
|
|
54
|
+
return (
|
|
55
|
+
<div className={cn("prose prose-sm dark:prose-invert max-w-none [&>p]:my-0", className)}>
|
|
56
|
+
<ReactMarkdown
|
|
57
|
+
remarkPlugins={[remarkGfm]}
|
|
58
|
+
components={{
|
|
59
|
+
// Code blocks with syntax highlighting
|
|
60
|
+
code({ className: codeClassName, children, ...props }) {
|
|
61
|
+
const match = /language-(\w+)/.exec(codeClassName || "");
|
|
62
|
+
const codeString = String(children).replace(/\n$/, "");
|
|
63
|
+
|
|
64
|
+
// Block code (with language tag)
|
|
65
|
+
if (match) {
|
|
66
|
+
return (
|
|
67
|
+
<div className="relative group not-prose my-3">
|
|
68
|
+
<div className="flex items-center justify-between px-4 py-1.5 bg-[#1e1e2e] rounded-t-lg border-b border-white/10">
|
|
69
|
+
<span className="text-xs text-white/40 font-mono">{match[1]}</span>
|
|
70
|
+
<CopyButton text={codeString} />
|
|
71
|
+
</div>
|
|
72
|
+
<SyntaxHighlighter
|
|
73
|
+
style={oneDark}
|
|
74
|
+
language={match[1]}
|
|
75
|
+
PreTag="div"
|
|
76
|
+
customStyle={{
|
|
77
|
+
margin: 0,
|
|
78
|
+
borderTopLeftRadius: 0,
|
|
79
|
+
borderTopRightRadius: 0,
|
|
80
|
+
borderBottomLeftRadius: "0.5rem",
|
|
81
|
+
borderBottomRightRadius: "0.5rem",
|
|
82
|
+
fontSize: "0.8125rem",
|
|
83
|
+
lineHeight: "1.5",
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{codeString}
|
|
87
|
+
</SyntaxHighlighter>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Multi-line code without language
|
|
93
|
+
if (codeString.includes("\n")) {
|
|
94
|
+
return (
|
|
95
|
+
<div className="relative group not-prose my-3">
|
|
96
|
+
<CopyButton text={codeString} />
|
|
97
|
+
<SyntaxHighlighter
|
|
98
|
+
style={oneDark}
|
|
99
|
+
PreTag="div"
|
|
100
|
+
customStyle={{
|
|
101
|
+
borderRadius: "0.5rem",
|
|
102
|
+
fontSize: "0.8125rem",
|
|
103
|
+
lineHeight: "1.5",
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
{codeString}
|
|
107
|
+
</SyntaxHighlighter>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Inline code
|
|
113
|
+
return (
|
|
114
|
+
<code
|
|
115
|
+
className="px-1.5 py-0.5 rounded bg-surface-hover font-mono text-[0.8125rem]"
|
|
116
|
+
{...props}
|
|
117
|
+
>
|
|
118
|
+
{children}
|
|
119
|
+
</code>
|
|
120
|
+
);
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// Links
|
|
124
|
+
a({ href, children }) {
|
|
125
|
+
return (
|
|
126
|
+
<a
|
|
127
|
+
href={href}
|
|
128
|
+
target="_blank"
|
|
129
|
+
rel="noopener noreferrer"
|
|
130
|
+
className="text-accent hover:underline"
|
|
131
|
+
>
|
|
132
|
+
{children}
|
|
133
|
+
</a>
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// Tables
|
|
138
|
+
table({ children }) {
|
|
139
|
+
return (
|
|
140
|
+
<div className="overflow-x-auto my-3 not-prose">
|
|
141
|
+
<table className="min-w-full text-sm border-collapse">
|
|
142
|
+
{children}
|
|
143
|
+
</table>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
th({ children }) {
|
|
148
|
+
return (
|
|
149
|
+
<th className="px-3 py-2 text-left text-xs font-medium text-foreground-secondary border-b border-border bg-surface-hover">
|
|
150
|
+
{children}
|
|
151
|
+
</th>
|
|
152
|
+
);
|
|
153
|
+
},
|
|
154
|
+
td({ children }) {
|
|
155
|
+
return (
|
|
156
|
+
<td className="px-3 py-2 text-sm border-b border-border/50">
|
|
157
|
+
{children}
|
|
158
|
+
</td>
|
|
159
|
+
);
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
// Blockquotes
|
|
163
|
+
blockquote({ children }) {
|
|
164
|
+
return (
|
|
165
|
+
<blockquote className="border-l-2 border-accent/50 pl-4 my-3 text-foreground-secondary italic">
|
|
166
|
+
{children}
|
|
167
|
+
</blockquote>
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
// Lists
|
|
172
|
+
ul({ children }) {
|
|
173
|
+
return <ul className="list-disc pl-5 my-2 space-y-1">{children}</ul>;
|
|
174
|
+
},
|
|
175
|
+
ol({ children }) {
|
|
176
|
+
return <ol className="list-decimal pl-5 my-2 space-y-1">{children}</ol>;
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// Horizontal rule
|
|
180
|
+
hr() {
|
|
181
|
+
return <hr className="my-4 border-border" />;
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// Paragraphs
|
|
185
|
+
p({ children }) {
|
|
186
|
+
return <p>{children}</p>;
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// Headings
|
|
190
|
+
h1({ children }) {
|
|
191
|
+
return <h1 className="text-lg font-bold mt-4 mb-2">{children}</h1>;
|
|
192
|
+
},
|
|
193
|
+
h2({ children }) {
|
|
194
|
+
return <h2 className="text-base font-bold mt-3 mb-1.5">{children}</h2>;
|
|
195
|
+
},
|
|
196
|
+
h3({ children }) {
|
|
197
|
+
return <h3 className="text-sm font-bold mt-2 mb-1">{children}</h3>;
|
|
198
|
+
},
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
{content}
|
|
202
|
+
</ReactMarkdown>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Bot, User, AlertTriangle, StopCircle } from "lucide-react";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { formatTime, formatDateTime } from "@/lib/date-utils";
|
|
7
|
+
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
|
8
|
+
import { Tooltip } from "@/components/ui/tooltip";
|
|
9
|
+
import { MarkdownContent } from "./markdown-content";
|
|
10
|
+
import { TwemojiText } from "@/components/ui/twemoji-text";
|
|
11
|
+
import type { ChatMessage } from "@/lib/types/chat";
|
|
12
|
+
import type { AgentInfo } from "./agent-mention-popup";
|
|
13
|
+
import type { AgentStatus } from "@/lib/hooks/use-agent-status";
|
|
14
|
+
|
|
15
|
+
interface MessageBubbleProps {
|
|
16
|
+
message: ChatMessage;
|
|
17
|
+
isAgent: boolean;
|
|
18
|
+
agentName?: string;
|
|
19
|
+
agentAvatar?: string | null;
|
|
20
|
+
userAvatar?: string | null;
|
|
21
|
+
agents: AgentInfo[];
|
|
22
|
+
isStreaming?: boolean;
|
|
23
|
+
/** Whether to show avatar and name (false for consecutive messages from same sender) */
|
|
24
|
+
showHeader?: boolean;
|
|
25
|
+
agentStatus?: AgentStatus;
|
|
26
|
+
userStatus?: AgentStatus;
|
|
27
|
+
highlighted?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function MessageBubble({
|
|
31
|
+
message,
|
|
32
|
+
isAgent,
|
|
33
|
+
agentName,
|
|
34
|
+
agentAvatar,
|
|
35
|
+
userAvatar,
|
|
36
|
+
agents,
|
|
37
|
+
isStreaming,
|
|
38
|
+
showHeader = true,
|
|
39
|
+
agentStatus,
|
|
40
|
+
userStatus,
|
|
41
|
+
highlighted,
|
|
42
|
+
}: MessageBubbleProps) {
|
|
43
|
+
const formattedTime = formatTime(message.createdAt);
|
|
44
|
+
const fullDateTime = formatDateTime(message.createdAt);
|
|
45
|
+
|
|
46
|
+
// Prefer the name stored on the message (always available from DB, no FOUC).
|
|
47
|
+
// Fall back to the live agent name from the gateway, then a generic label.
|
|
48
|
+
const displayName = isAgent
|
|
49
|
+
? (message.senderName || agentName || "Unknown Agent")
|
|
50
|
+
: "You";
|
|
51
|
+
const avatarSrc = isAgent ? agentAvatar : userAvatar;
|
|
52
|
+
const hasAttachments = message.attachments && message.attachments.length > 0;
|
|
53
|
+
|
|
54
|
+
// Map status to Avatar status prop
|
|
55
|
+
const avatarStatus = isAgent && agentStatus
|
|
56
|
+
? ({ thinking: "away", active: "online", idle: "offline" } as const)[agentStatus]
|
|
57
|
+
: !isAgent && userStatus
|
|
58
|
+
? ({ active: "online", idle: "offline", thinking: "away" } as const)[userStatus]
|
|
59
|
+
: undefined;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className={cn("flex gap-3", showHeader ? "mb-[4px] first:mt-0" : "mt-0.5 pl-[48px]", highlighted && "bg-accent/10 -mx-2 px-2 py-1")}>
|
|
63
|
+
{/* Avatar — only shown on first message in a group */}
|
|
64
|
+
{showHeader && (
|
|
65
|
+
<div className="mt-0.5">
|
|
66
|
+
<Avatar size="sm" status={avatarStatus} statusPulse={agentStatus === "thinking"}>
|
|
67
|
+
{avatarSrc ? (
|
|
68
|
+
<AvatarImage src={avatarSrc} alt={displayName} />
|
|
69
|
+
) : (
|
|
70
|
+
<AvatarFallback
|
|
71
|
+
className={cn(
|
|
72
|
+
isAgent ? "bg-accent/20 text-accent" : "bg-foreground/10 text-foreground"
|
|
73
|
+
)}
|
|
74
|
+
>
|
|
75
|
+
{isAgent ? <Bot className="w-4 h-4" /> : <User className="w-4 h-4" />}
|
|
76
|
+
</AvatarFallback>
|
|
77
|
+
)}
|
|
78
|
+
</Avatar>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
{/* Message content */}
|
|
83
|
+
<div className="flex flex-col min-w-0">
|
|
84
|
+
{/* Name + time header — only on first message in a group */}
|
|
85
|
+
{showHeader && (
|
|
86
|
+
<div className="flex items-baseline gap-2">
|
|
87
|
+
<span className="font-bold text-[15px] text-foreground">
|
|
88
|
+
{displayName}
|
|
89
|
+
</span>
|
|
90
|
+
<Tooltip content={fullDateTime} side="top" delay={800}>
|
|
91
|
+
<span className="text-xs text-foreground-secondary hover:text-foreground cursor-pointer transition-colors">
|
|
92
|
+
{formattedTime}
|
|
93
|
+
</span>
|
|
94
|
+
</Tooltip>
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{/* Attachments */}
|
|
99
|
+
{hasAttachments && (
|
|
100
|
+
<div className="flex gap-2 flex-wrap mb-1">
|
|
101
|
+
{message.attachments.map((att) => (
|
|
102
|
+
<img
|
|
103
|
+
key={att.id}
|
|
104
|
+
src={`/api/openclaw/chat/attachments?path=${encodeURIComponent(att.filePath)}`}
|
|
105
|
+
alt={att.originalName || "Attachment"}
|
|
106
|
+
className="max-w-[200px] max-h-[200px] rounded-lg object-cover border border-border"
|
|
107
|
+
/>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{/* Message text — no bubble, just plain text */}
|
|
113
|
+
<div className="text-[15px] text-foreground leading-[26px] break-words">
|
|
114
|
+
<TwemojiText>
|
|
115
|
+
{isAgent && message.content ? (
|
|
116
|
+
<MarkdownContent content={message.content} />
|
|
117
|
+
) : (
|
|
118
|
+
<span className="whitespace-pre-wrap">{message.content}</span>
|
|
119
|
+
)}
|
|
120
|
+
</TwemojiText>
|
|
121
|
+
|
|
122
|
+
{/* Streaming cursor */}
|
|
123
|
+
{isStreaming && (
|
|
124
|
+
<span className="inline-block w-2 h-4 bg-foreground/50 animate-pulse ml-0.5 align-text-bottom" />
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Status badges */}
|
|
129
|
+
{message.status === "interrupted" && (
|
|
130
|
+
<div className="flex items-center gap-1 text-xs text-warning mt-1">
|
|
131
|
+
<AlertTriangle className="h-3 w-3" />
|
|
132
|
+
<span>Response interrupted</span>
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
{message.status === "aborted" && (
|
|
136
|
+
<div className="flex items-center gap-1 text-xs text-foreground-secondary mt-1">
|
|
137
|
+
<StopCircle className="h-3 w-3" />
|
|
138
|
+
<span>Response stopped</span>
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
{/* Token info for agent messages */}
|
|
143
|
+
{isAgent && !isStreaming && (message.inputTokens || message.outputTokens) && (
|
|
144
|
+
<div className="text-xs text-foreground-secondary/60 flex items-center gap-2 mt-1">
|
|
145
|
+
{message.inputTokens != null && <span>{message.inputTokens.toLocaleString()} in</span>}
|
|
146
|
+
{message.outputTokens != null && <span>{message.outputTokens.toLocaleString()} out</span>}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|