S2: Colony chat UI — dark industrial design, JetBrains Mono
- Channel sidebar with create - Message timeline with type-based styling (code/result/error/plan) - Agent messages get glow line + AGENT badge - Agent metadata strip (model, hostname, cwd, skill) - Reply-to with context preview - Compose box with message type selector (Alt+1-5) - 3s polling for live updates (WebSocket in S5) - Vite proxy to backend, TypeScript strict mode, Biome linting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
110
ui/colony/src/components/ComposeBox.tsx
Normal file
110
ui/colony/src/components/ComposeBox.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { useState } from "react";
|
||||
import type { MessageType } from "@/types/MessageType";
|
||||
import { postMessage } from "@/api";
|
||||
|
||||
interface Props {
|
||||
channelId: string;
|
||||
replyTo: string | null;
|
||||
onClearReply: () => void;
|
||||
onMessageSent: () => void;
|
||||
}
|
||||
|
||||
const TYPES: { value: MessageType; label: string; key: string }[] = [
|
||||
{ value: "text", label: "TXT", key: "1" },
|
||||
{ value: "code", label: "CODE", key: "2" },
|
||||
{ value: "result", label: "RES", key: "3" },
|
||||
{ value: "error", label: "ERR", key: "4" },
|
||||
{ value: "plan", label: "PLAN", key: "5" },
|
||||
];
|
||||
|
||||
export function ComposeBox({
|
||||
channelId,
|
||||
replyTo,
|
||||
onClearReply,
|
||||
onMessageSent,
|
||||
}: Props) {
|
||||
const [content, setContent] = useState("");
|
||||
const [msgType, setMsgType] = useState<MessageType>("text");
|
||||
const [sending, setSending] = useState(false);
|
||||
|
||||
async function handleSend() {
|
||||
if (!content.trim() || sending) return;
|
||||
setSending(true);
|
||||
await postMessage(channelId, {
|
||||
content: content.trim(),
|
||||
type: msgType,
|
||||
reply_to: replyTo ?? undefined,
|
||||
});
|
||||
setContent("");
|
||||
setMsgType("text");
|
||||
onClearReply();
|
||||
onMessageSent();
|
||||
setSending(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-t border-border bg-card px-4 py-3">
|
||||
{replyTo && (
|
||||
<div className="flex items-center gap-2 mb-2 text-[11px] text-muted-foreground">
|
||||
<span>replying to #{replyTo.slice(0, 8)}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClearReply}
|
||||
className="text-[10px] hover:text-foreground"
|
||||
>
|
||||
[x]
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Type selector */}
|
||||
<div className="flex gap-0.5">
|
||||
{TYPES.map((t) => (
|
||||
<button
|
||||
type="button"
|
||||
key={t.value}
|
||||
onClick={() => setMsgType(t.value)}
|
||||
className={`px-1.5 py-0.5 text-[10px] font-bold rounded-sm transition-colors ${
|
||||
msgType === t.value
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
title={`${t.label} (Alt+${t.key})`}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<input
|
||||
type="text"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
// Alt+1-5 for type switching
|
||||
if (e.altKey && e.key >= "1" && e.key <= "5") {
|
||||
setMsgType(TYPES[parseInt(e.key) - 1].value);
|
||||
}
|
||||
}}
|
||||
placeholder={`message #${channelId.slice(0, 8)}...`}
|
||||
disabled={sending}
|
||||
className="flex-1 bg-input text-[13px] text-foreground placeholder:text-muted-foreground px-3 py-1.5 rounded-sm border border-border focus:outline-none focus:border-[var(--color-agent-glow)]"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSend}
|
||||
disabled={sending || !content.trim()}
|
||||
className="px-3 py-1.5 text-[11px] font-bold bg-primary text-primary-foreground rounded-sm hover:opacity-80 disabled:opacity-30 transition-opacity"
|
||||
>
|
||||
SEND
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user