Skip to content

Commit a6ce6fa

Browse files
authored
Merge branch 'main' into bugfix/slack-tls-fix
2 parents c68e77c + d04d882 commit a6ce6fa

File tree

3 files changed

+312
-192
lines changed

3 files changed

+312
-192
lines changed

interface/src/components/CortexChatPanel.tsx

Lines changed: 131 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ function ToolActivityIndicator({ activity }: { activity: ToolActivity[] }) {
1515
if (activity.length === 0) return null;
1616

1717
return (
18-
<div className="flex flex-col gap-1 px-3 py-2">
18+
<div className="flex flex-wrap items-center gap-1.5 mt-2">
1919
{activity.map((tool, index) => (
20-
<div
20+
<span
2121
key={`${tool.tool}-${index}`}
22-
className="flex items-center gap-2 rounded bg-app-darkBox/40 px-2 py-1"
22+
className="inline-flex items-center gap-1.5 rounded-full bg-app-box/60 px-2.5 py-0.5"
2323
>
2424
{tool.status === "running" ? (
2525
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-amber-400" />
@@ -28,60 +28,141 @@ function ToolActivityIndicator({ activity }: { activity: ToolActivity[] }) {
2828
)}
2929
<span className="font-mono text-tiny text-ink-faint">{tool.tool}</span>
3030
{tool.status === "done" && tool.result_preview && (
31-
<span className="min-w-0 flex-1 truncate text-tiny text-ink-faint/60">
31+
<span className="min-w-0 max-w-[120px] truncate text-tiny text-ink-faint/60">
3232
{tool.result_preview.slice(0, 80)}
3333
</span>
3434
)}
35-
</div>
35+
</span>
3636
))}
3737
</div>
3838
);
3939
}
4040

41+
function ThinkingIndicator() {
42+
return (
43+
<div className="flex items-center gap-1.5 py-1">
44+
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-ink-faint" />
45+
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-ink-faint [animation-delay:0.2s]" />
46+
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-ink-faint [animation-delay:0.4s]" />
47+
</div>
48+
);
49+
}
50+
51+
function CortexChatInput({
52+
value,
53+
onChange,
54+
onSubmit,
55+
isStreaming,
56+
}: {
57+
value: string;
58+
onChange: (value: string) => void;
59+
onSubmit: () => void;
60+
isStreaming: boolean;
61+
}) {
62+
const textareaRef = useRef<HTMLTextAreaElement>(null);
63+
64+
useEffect(() => {
65+
textareaRef.current?.focus();
66+
}, []);
67+
68+
useEffect(() => {
69+
const textarea = textareaRef.current;
70+
if (!textarea) return;
71+
72+
const adjustHeight = () => {
73+
textarea.style.height = "auto";
74+
const scrollHeight = textarea.scrollHeight;
75+
const maxHeight = 160;
76+
textarea.style.height = `${Math.min(scrollHeight, maxHeight)}px`;
77+
textarea.style.overflowY = scrollHeight > maxHeight ? "auto" : "hidden";
78+
};
79+
80+
adjustHeight();
81+
textarea.addEventListener("input", adjustHeight);
82+
return () => textarea.removeEventListener("input", adjustHeight);
83+
}, [value]);
84+
85+
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
86+
if (event.key === "Enter" && !event.shiftKey) {
87+
event.preventDefault();
88+
onSubmit();
89+
}
90+
};
91+
92+
return (
93+
<div className="rounded-xl border border-app-line/50 bg-app-box/40 backdrop-blur-xl transition-colors duration-200 hover:border-app-line/70">
94+
<div className="flex items-end gap-2 p-2.5">
95+
<textarea
96+
ref={textareaRef}
97+
value={value}
98+
onChange={(event) => onChange(event.target.value)}
99+
onKeyDown={handleKeyDown}
100+
placeholder={isStreaming ? "Waiting for response..." : "Message the cortex..."}
101+
disabled={isStreaming}
102+
rows={1}
103+
className="flex-1 resize-none bg-transparent px-1 py-1 text-sm text-ink placeholder:text-ink-faint/60 focus:outline-none disabled:opacity-40"
104+
style={{ maxHeight: "160px" }}
105+
/>
106+
<button
107+
type="button"
108+
onClick={onSubmit}
109+
disabled={isStreaming || !value.trim()}
110+
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-accent text-white transition-all duration-150 hover:bg-accent-deep disabled:opacity-30 disabled:hover:bg-accent"
111+
>
112+
<svg
113+
width="14"
114+
height="14"
115+
viewBox="0 0 24 24"
116+
fill="none"
117+
stroke="currentColor"
118+
strokeWidth="2"
119+
strokeLinecap="round"
120+
strokeLinejoin="round"
121+
>
122+
<path d="M12 19V5M5 12l7-7 7 7" />
123+
</svg>
124+
</button>
125+
</div>
126+
</div>
127+
);
128+
}
129+
41130
export function CortexChatPanel({ agentId, channelId, onClose }: CortexChatPanelProps) {
42131
const { messages, isStreaming, error, toolActivity, sendMessage, newThread } = useCortexChat(agentId, channelId);
43132
const [input, setInput] = useState("");
44133
const messagesEndRef = useRef<HTMLDivElement>(null);
45-
const inputRef = useRef<HTMLInputElement>(null);
46134

47-
// Auto-scroll on new messages or tool activity
48135
useEffect(() => {
49136
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
50137
}, [messages.length, isStreaming, toolActivity.length]);
51138

52-
// Focus input on mount
53-
useEffect(() => {
54-
inputRef.current?.focus();
55-
}, []);
56-
57-
const handleSubmit = (event: React.FormEvent) => {
58-
event.preventDefault();
139+
const handleSubmit = () => {
59140
const trimmed = input.trim();
60141
if (!trimmed || isStreaming) return;
61142
setInput("");
62143
sendMessage(trimmed);
63144
};
64145

65146
return (
66-
<div className="flex h-full w-full flex-col bg-app-darkBox/30">
147+
<div className="flex h-full w-full flex-col">
67148
{/* Header */}
68-
<div className="flex h-12 items-center justify-between border-b border-app-line/50 px-4">
149+
<div className="flex h-10 items-center justify-between border-b border-app-line/50 px-3">
69150
<div className="flex items-center gap-2">
70151
<span className="text-sm font-medium text-ink">Cortex</span>
71152
{channelId && (
72-
<span className="rounded bg-violet-500/10 px-1.5 py-0.5 text-tiny text-violet-400">
73-
{channelId.length > 24 ? `${channelId.slice(0, 24)}...` : channelId}
153+
<span className="rounded-full bg-app-box px-2 py-0.5 text-tiny text-ink-faint">
154+
{channelId.length > 20 ? `${channelId.slice(0, 20)}...` : channelId}
74155
</span>
75156
)}
76157
</div>
77-
<div className="flex items-center gap-1">
158+
<div className="flex items-center gap-0.5">
78159
<Button
79160
onClick={newThread}
80161
variant="ghost"
81162
size="icon"
82163
disabled={isStreaming}
83164
className="h-7 w-7"
84-
title="New chat"
165+
title="New thread"
85166
>
86167
<HugeiconsIcon icon={PlusSignIcon} className="h-3.5 w-3.5" />
87168
</Button>
@@ -101,51 +182,39 @@ export function CortexChatPanel({ agentId, channelId, onClose }: CortexChatPanel
101182

102183
{/* Messages */}
103184
<div className="flex-1 overflow-y-auto">
104-
<div className="flex flex-col gap-3 p-4">
185+
<div className="flex flex-col gap-5 p-3 pb-4">
105186
{messages.length === 0 && !isStreaming && (
106-
<p className="py-8 text-center text-sm text-ink-faint">
107-
Ask the cortex anything
108-
</p>
187+
<div className="flex items-center justify-center py-12">
188+
<p className="text-sm text-ink-faint">Ask the cortex anything</p>
189+
</div>
109190
)}
191+
110192
{messages.map((message) => (
111-
<div
112-
key={message.id}
113-
className={`rounded-md px-3 py-2 ${
114-
message.role === "user"
115-
? "ml-8 bg-accent/10"
116-
: "mr-2 bg-app-darkBox/50"
117-
}`}
118-
>
119-
<span className={`text-tiny font-medium ${
120-
message.role === "user" ? "text-accent-faint" : "text-violet-400"
121-
}`}>
122-
{message.role === "user" ? "admin" : "cortex"}
123-
</span>
124-
<div className="mt-0.5 text-sm text-ink-dull">
125-
{message.role === "assistant" ? (
193+
<div key={message.id}>
194+
{message.role === "user" ? (
195+
<div className="flex justify-end">
196+
<div className="max-w-[85%] rounded-2xl rounded-br-md bg-accent/10 px-3 py-2">
197+
<p className="text-sm text-ink">{message.content}</p>
198+
</div>
199+
</div>
200+
) : (
201+
<div className="text-sm text-ink-dull">
126202
<Markdown>{message.content}</Markdown>
127-
) : (
128-
<p>{message.content}</p>
129-
)}
130-
</div>
203+
</div>
204+
)}
131205
</div>
132206
))}
207+
208+
{/* Streaming state */}
133209
{isStreaming && (
134-
<div className="mr-2 rounded-md bg-app-darkBox/50 px-3 py-2">
135-
<span className="text-tiny font-medium text-violet-400">cortex</span>
210+
<div>
136211
<ToolActivityIndicator activity={toolActivity} />
137-
{toolActivity.length === 0 && (
138-
<div className="mt-1 flex items-center gap-1">
139-
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-violet-400" />
140-
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-violet-400 [animation-delay:0.2s]" />
141-
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-violet-400 [animation-delay:0.4s]" />
142-
<span className="ml-1 text-tiny text-ink-faint">thinking...</span>
143-
</div>
144-
)}
212+
{toolActivity.length === 0 && <ThinkingIndicator />}
145213
</div>
146214
)}
215+
147216
{error && (
148-
<div className="rounded-md border border-red-500/20 bg-red-500/10 px-3 py-2 text-sm text-red-400">
217+
<div className="rounded-lg border border-red-500/20 bg-red-500/5 px-3 py-2.5 text-sm text-red-400">
149218
{error}
150219
</div>
151220
)}
@@ -154,27 +223,14 @@ export function CortexChatPanel({ agentId, channelId, onClose }: CortexChatPanel
154223
</div>
155224

156225
{/* Input */}
157-
<form onSubmit={handleSubmit} className="border-t border-app-line/50 p-3">
158-
<div className="flex gap-2">
159-
<input
160-
ref={inputRef}
161-
type="text"
162-
value={input}
163-
onChange={(event) => setInput(event.target.value)}
164-
placeholder={isStreaming ? "Waiting for response..." : "Message the cortex..."}
165-
disabled={isStreaming}
166-
className="flex-1 rounded-md border border-app-line bg-app-darkBox px-3 py-1.5 text-sm text-ink placeholder:text-ink-faint focus:border-violet-500/50 focus:outline-none disabled:opacity-50"
167-
/>
168-
<Button
169-
type="submit"
170-
disabled={isStreaming || !input.trim()}
171-
size="sm"
172-
className="bg-violet-500/20 text-violet-400 hover:bg-violet-500/30"
173-
>
174-
Send
175-
</Button>
176-
</div>
177-
</form>
226+
<div className="border-t border-app-line/50 p-3">
227+
<CortexChatInput
228+
value={input}
229+
onChange={setInput}
230+
onSubmit={handleSubmit}
231+
isStreaming={isStreaming}
232+
/>
233+
</div>
178234
</div>
179235
);
180236
}

0 commit comments

Comments
 (0)