cue-console 0.1.10 → 0.1.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/README.md +1 -1
- package/package.json +2 -2
- package/src/components/chat/avatar-picker-dialog.tsx +84 -0
- package/src/components/chat/chat-header.tsx +132 -0
- package/src/components/chat/image-preview-dialog.tsx +34 -0
- package/src/components/chat/message-bubble.tsx +186 -0
- package/src/components/chat/timeline-list.tsx +470 -0
- package/src/components/chat/user-response-bubble.tsx +146 -0
- package/src/components/chat-view.tsx +195 -1702
- package/src/components/conversation-list.tsx +74 -3
- package/src/components/ui/scroll-area.tsx +9 -4
- package/src/contexts/chat-providers.tsx +20 -0
- package/src/contexts/input-context.tsx +37 -0
- package/src/contexts/ui-state-context.tsx +45 -0
- package/src/hooks/use-audio-notification.ts +51 -0
- package/src/hooks/use-avatar-management.ts +83 -0
- package/src/hooks/use-avatar.ts +149 -0
- package/src/hooks/use-conversation-timeline.ts +226 -0
- package/src/hooks/use-draft-persistence.ts +73 -0
- package/src/hooks/use-file-handler.ts +117 -0
- package/src/hooks/use-mentions.ts +407 -0
- package/src/hooks/use-message-queue.ts +231 -0
- package/src/hooks/use-message-sender.ts +84 -0
- package/src/lib/actions.ts +25 -11
- package/src/lib/chat-logic.ts +104 -0
- package/src/lib/db.ts +213 -2
- package/src/lib/error-handler.ts +48 -0
- package/src/lib/file-utils.ts +96 -0
- package/src/types/chat.ts +47 -0
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ Add the contents of `cue-command/protocol.md` to your tool's system prompt / rul
|
|
|
66
66
|
|
|
67
67
|
### Step 5: Connect your runtime
|
|
68
68
|
|
|
69
|
-
In the agent/runtime you want to use, call `cueme cue
|
|
69
|
+
In the agent/runtime you want to use, call `cueme cue <agent_id> -` / `cueme pause <agent_id> -` (see `cue-command/protocol.md`).
|
|
70
70
|
|
|
71
71
|
---
|
|
72
72
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cue-console",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Cue Hub console launcher (Next.js UI)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": ["mcp", "cue", "console", "nextjs"],
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"@radix-ui/react-separator": "^1.1.8",
|
|
34
34
|
"@radix-ui/react-slot": "^1.2.4",
|
|
35
35
|
"@tailwindcss/postcss": "^4",
|
|
36
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
36
37
|
"@types/node": "^20",
|
|
37
38
|
"@dicebear/core": "^9.2.4",
|
|
38
39
|
"@dicebear/thumbs": "^9.2.4",
|
|
@@ -54,7 +55,6 @@
|
|
|
54
55
|
"uuid": "^13.0.0"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
|
-
"@types/better-sqlite3": "^7.6.13",
|
|
58
58
|
"@types/react": "^19",
|
|
59
59
|
"@types/react-dom": "^19",
|
|
60
60
|
"@types/uuid": "^11.0.0",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
} from "@/components/ui/dialog";
|
|
10
|
+
import { getAgentEmoji } from "@/lib/utils";
|
|
11
|
+
|
|
12
|
+
interface AvatarPickerDialogProps {
|
|
13
|
+
open: boolean;
|
|
14
|
+
onOpenChange: (open: boolean) => void;
|
|
15
|
+
target: { kind: "agent" | "group"; id: string } | null;
|
|
16
|
+
titleDisplay: string;
|
|
17
|
+
currentAvatarUrl: string;
|
|
18
|
+
candidates: { seed: string; url: string }[];
|
|
19
|
+
onRandomize: () => void;
|
|
20
|
+
onSelect: (seed: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function AvatarPickerDialog({
|
|
24
|
+
open,
|
|
25
|
+
onOpenChange,
|
|
26
|
+
target,
|
|
27
|
+
titleDisplay,
|
|
28
|
+
currentAvatarUrl,
|
|
29
|
+
candidates,
|
|
30
|
+
onRandomize,
|
|
31
|
+
onSelect,
|
|
32
|
+
}: AvatarPickerDialogProps) {
|
|
33
|
+
return (
|
|
34
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
35
|
+
<DialogContent className="max-w-lg glass-surface glass-noise">
|
|
36
|
+
<DialogHeader>
|
|
37
|
+
<DialogTitle>Avatar</DialogTitle>
|
|
38
|
+
</DialogHeader>
|
|
39
|
+
{target && (
|
|
40
|
+
<div className="flex flex-col gap-4">
|
|
41
|
+
<div className="flex items-center gap-3">
|
|
42
|
+
<div className="h-14 w-14 rounded-full bg-muted overflow-hidden">
|
|
43
|
+
{currentAvatarUrl ? (
|
|
44
|
+
<img src={currentAvatarUrl} alt="" className="h-full w-full" />
|
|
45
|
+
) : (
|
|
46
|
+
<div className="flex h-full w-full items-center justify-center text-xl">
|
|
47
|
+
{target.kind === "group" ? "👥" : getAgentEmoji(target.id)}
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
<div className="flex-1 min-w-0">
|
|
52
|
+
<p className="text-sm font-semibold truncate">{titleDisplay}</p>
|
|
53
|
+
<p className="text-xs text-muted-foreground truncate">
|
|
54
|
+
Click a thumb to apply
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
<Button variant="outline" size="sm" onClick={onRandomize}>
|
|
58
|
+
Random
|
|
59
|
+
</Button>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="max-h-52 overflow-y-auto pr-1">
|
|
63
|
+
<div className="grid grid-cols-5 gap-2">
|
|
64
|
+
{candidates.map((c) => (
|
|
65
|
+
<button
|
|
66
|
+
key={c.seed}
|
|
67
|
+
type="button"
|
|
68
|
+
className="h-12 w-12 rounded-full bg-muted overflow-hidden hover:ring-2 hover:ring-ring/40"
|
|
69
|
+
onClick={() => onSelect(c.seed)}
|
|
70
|
+
title="Apply"
|
|
71
|
+
>
|
|
72
|
+
{c.url ? (
|
|
73
|
+
<img src={c.url} alt="" className="h-full w-full" />
|
|
74
|
+
) : null}
|
|
75
|
+
</button>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</DialogContent>
|
|
82
|
+
</Dialog>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { cn, getAgentEmoji } from "@/lib/utils";
|
|
6
|
+
import { ChevronLeft, Github } from "lucide-react";
|
|
7
|
+
|
|
8
|
+
interface ChatHeaderProps {
|
|
9
|
+
type: "agent" | "group";
|
|
10
|
+
id: string;
|
|
11
|
+
titleDisplay: string;
|
|
12
|
+
avatarUrl: string;
|
|
13
|
+
members: string[];
|
|
14
|
+
onBack?: () => void;
|
|
15
|
+
onAvatarClick: () => void;
|
|
16
|
+
onTitleChange: (newTitle: string) => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ChatHeader({
|
|
20
|
+
type,
|
|
21
|
+
id,
|
|
22
|
+
titleDisplay,
|
|
23
|
+
avatarUrl,
|
|
24
|
+
members,
|
|
25
|
+
onBack,
|
|
26
|
+
onAvatarClick,
|
|
27
|
+
onTitleChange,
|
|
28
|
+
}: ChatHeaderProps) {
|
|
29
|
+
const [editingTitle, setEditingTitle] = useState(false);
|
|
30
|
+
const [titleDraft, setTitleDraft] = useState("");
|
|
31
|
+
|
|
32
|
+
const beginEditTitle = () => {
|
|
33
|
+
setEditingTitle(true);
|
|
34
|
+
setTitleDraft(titleDisplay);
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
const el = document.getElementById("chat-title-input");
|
|
37
|
+
if (el instanceof HTMLInputElement) el.focus();
|
|
38
|
+
}, 0);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const commitEditTitle = async () => {
|
|
42
|
+
const next = titleDraft.trim();
|
|
43
|
+
setEditingTitle(false);
|
|
44
|
+
if (!next || next === titleDisplay) return;
|
|
45
|
+
await onTitleChange(next);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className={cn("px-4 pt-4")}>
|
|
50
|
+
<div
|
|
51
|
+
className={cn(
|
|
52
|
+
"mx-auto flex w-full max-w-230 items-center gap-2 rounded-3xl p-3",
|
|
53
|
+
"glass-surface glass-noise"
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
{onBack && (
|
|
57
|
+
<Button variant="ghost" size="icon" onClick={onBack}>
|
|
58
|
+
<ChevronLeft className="h-5 w-5" />
|
|
59
|
+
</Button>
|
|
60
|
+
)}
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
className="h-9 w-9 shrink-0 rounded-full bg-muted overflow-hidden"
|
|
64
|
+
onClick={onAvatarClick}
|
|
65
|
+
title="Change avatar"
|
|
66
|
+
>
|
|
67
|
+
{avatarUrl ? (
|
|
68
|
+
<img src={avatarUrl} alt="" className="h-full w-full" />
|
|
69
|
+
) : (
|
|
70
|
+
<span className="flex h-full w-full items-center justify-center text-lg">
|
|
71
|
+
{type === "group" ? "👥" : getAgentEmoji(id)}
|
|
72
|
+
</span>
|
|
73
|
+
)}
|
|
74
|
+
</button>
|
|
75
|
+
<div className="flex-1 min-w-0">
|
|
76
|
+
{editingTitle ? (
|
|
77
|
+
<input
|
|
78
|
+
id="chat-title-input"
|
|
79
|
+
value={titleDraft}
|
|
80
|
+
onChange={(e) => setTitleDraft(e.target.value)}
|
|
81
|
+
onKeyDown={(e) => {
|
|
82
|
+
if (e.key === "Enter") {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
void commitEditTitle();
|
|
85
|
+
}
|
|
86
|
+
if (e.key === "Escape") {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
setEditingTitle(false);
|
|
89
|
+
}
|
|
90
|
+
}}
|
|
91
|
+
onBlur={() => {
|
|
92
|
+
void commitEditTitle();
|
|
93
|
+
}}
|
|
94
|
+
className="w-60 max-w-full rounded-xl border border-white/45 bg-white/55 px-2.5 py-1.5 text-sm font-semibold outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]"
|
|
95
|
+
/>
|
|
96
|
+
) : (
|
|
97
|
+
<h2
|
|
98
|
+
className={cn("font-semibold", "cursor-text", "truncate")}
|
|
99
|
+
onDoubleClick={beginEditTitle}
|
|
100
|
+
title="Double-click to rename"
|
|
101
|
+
>
|
|
102
|
+
{titleDisplay}
|
|
103
|
+
</h2>
|
|
104
|
+
)}
|
|
105
|
+
{type === "group" && members.length > 0 && (
|
|
106
|
+
<p className="text-xs text-muted-foreground truncate">
|
|
107
|
+
{members.length} member{members.length === 1 ? "" : "s"}
|
|
108
|
+
</p>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
<Button variant="ghost" size="icon" asChild>
|
|
112
|
+
<a
|
|
113
|
+
href="https://github.com/nmhjklnm/cue-console"
|
|
114
|
+
target="_blank"
|
|
115
|
+
rel="noreferrer"
|
|
116
|
+
title="https://github.com/nmhjklnm/cue-console"
|
|
117
|
+
>
|
|
118
|
+
<Github className="h-5 w-5" />
|
|
119
|
+
</a>
|
|
120
|
+
</Button>
|
|
121
|
+
{type === "group" && (
|
|
122
|
+
<span
|
|
123
|
+
className="hidden sm:inline text-[11px] text-muted-foreground select-none mr-1"
|
|
124
|
+
title="Type @ to mention members"
|
|
125
|
+
>
|
|
126
|
+
@ mention
|
|
127
|
+
</span>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogHeader,
|
|
7
|
+
DialogTitle,
|
|
8
|
+
} from "@/components/ui/dialog";
|
|
9
|
+
|
|
10
|
+
interface ImagePreviewDialogProps {
|
|
11
|
+
image: { mime_type: string; base64_data: string } | null;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ImagePreviewDialog({ image, onClose }: ImagePreviewDialogProps) {
|
|
16
|
+
return (
|
|
17
|
+
<Dialog open={!!image} onOpenChange={(open) => !open && onClose()}>
|
|
18
|
+
<DialogContent className="max-w-3xl glass-surface glass-noise">
|
|
19
|
+
<DialogHeader>
|
|
20
|
+
<DialogTitle>Preview</DialogTitle>
|
|
21
|
+
</DialogHeader>
|
|
22
|
+
{image && (
|
|
23
|
+
<div className="flex items-center justify-center">
|
|
24
|
+
<img
|
|
25
|
+
src={`data:${image.mime_type};base64,${image.base64_data}`}
|
|
26
|
+
alt=""
|
|
27
|
+
className="max-h-[70vh] rounded-lg"
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
)}
|
|
31
|
+
</DialogContent>
|
|
32
|
+
</Dialog>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { Badge } from "@/components/ui/badge";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { cn, getAgentEmoji, formatFullTime, getWaitingDuration } from "@/lib/utils";
|
|
5
|
+
import { MarkdownRenderer } from "@/components/markdown-renderer";
|
|
6
|
+
import { PayloadCard } from "@/components/payload-card";
|
|
7
|
+
import type { CueRequest } from "@/lib/actions";
|
|
8
|
+
|
|
9
|
+
interface MessageBubbleProps {
|
|
10
|
+
request: CueRequest;
|
|
11
|
+
showAgent?: boolean;
|
|
12
|
+
agentNameMap?: Record<string, string>;
|
|
13
|
+
avatarUrlMap?: Record<string, string>;
|
|
14
|
+
isHistory?: boolean;
|
|
15
|
+
showName?: boolean;
|
|
16
|
+
showAvatar?: boolean;
|
|
17
|
+
compact?: boolean;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
currentInput?: string;
|
|
20
|
+
isGroup?: boolean;
|
|
21
|
+
onPasteChoice?: (text: string, mode?: "replace" | "append" | "upsert") => void;
|
|
22
|
+
onSubmitConfirm?: (requestId: string, text: string, cancelled: boolean) => void | Promise<void>;
|
|
23
|
+
onMentionAgent?: (agentId: string) => void;
|
|
24
|
+
onReply?: () => void;
|
|
25
|
+
onCancel?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function MessageBubble({
|
|
29
|
+
request,
|
|
30
|
+
showAgent,
|
|
31
|
+
agentNameMap,
|
|
32
|
+
avatarUrlMap,
|
|
33
|
+
isHistory,
|
|
34
|
+
showName,
|
|
35
|
+
showAvatar,
|
|
36
|
+
compact,
|
|
37
|
+
disabled,
|
|
38
|
+
currentInput,
|
|
39
|
+
isGroup,
|
|
40
|
+
onPasteChoice,
|
|
41
|
+
onSubmitConfirm,
|
|
42
|
+
onMentionAgent,
|
|
43
|
+
onReply,
|
|
44
|
+
onCancel,
|
|
45
|
+
}: MessageBubbleProps) {
|
|
46
|
+
const isPending = request.status === "PENDING";
|
|
47
|
+
|
|
48
|
+
const isPause = useMemo(() => {
|
|
49
|
+
if (!request.payload) return false;
|
|
50
|
+
try {
|
|
51
|
+
const obj = JSON.parse(request.payload) as Record<string, unknown>;
|
|
52
|
+
return obj?.type === "confirm" && obj?.variant === "pause";
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}, [request.payload]);
|
|
57
|
+
|
|
58
|
+
const selectedLines = useMemo(() => {
|
|
59
|
+
const text = (currentInput || "").trim();
|
|
60
|
+
if (!text) return new Set<string>();
|
|
61
|
+
return new Set(
|
|
62
|
+
text
|
|
63
|
+
.split(/\r?\n/)
|
|
64
|
+
.map((s) => s.trim())
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
);
|
|
67
|
+
}, [currentInput]);
|
|
68
|
+
|
|
69
|
+
const rawId = request.agent_id || "";
|
|
70
|
+
const displayName = (agentNameMap && rawId ? agentNameMap[rawId] || rawId : rawId) || "";
|
|
71
|
+
const cardMaxWidth = (showAvatar ?? true) ? "calc(100% - 3rem)" : "100%";
|
|
72
|
+
const avatarUrl = rawId && avatarUrlMap ? avatarUrlMap[`agent:${rawId}`] : "";
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
className={cn(
|
|
77
|
+
"flex max-w-full min-w-0 items-start gap-3",
|
|
78
|
+
compact && "gap-2",
|
|
79
|
+
isHistory && "opacity-60"
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{(showAvatar ?? true) ? (
|
|
83
|
+
<span
|
|
84
|
+
className={cn(
|
|
85
|
+
"flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-muted text-lg",
|
|
86
|
+
isGroup && request.agent_id && onMentionAgent && "cursor-pointer"
|
|
87
|
+
)}
|
|
88
|
+
title={
|
|
89
|
+
isGroup && request.agent_id && onMentionAgent
|
|
90
|
+
? "Double-click avatar to @mention"
|
|
91
|
+
: undefined
|
|
92
|
+
}
|
|
93
|
+
onDoubleClick={() => {
|
|
94
|
+
if (!isGroup) return;
|
|
95
|
+
const agentId = request.agent_id;
|
|
96
|
+
if (!agentId) return;
|
|
97
|
+
onMentionAgent?.(agentId);
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
{avatarUrl ? (
|
|
101
|
+
<img src={avatarUrl} alt="" className="h-full w-full rounded-full" />
|
|
102
|
+
) : (
|
|
103
|
+
getAgentEmoji(request.agent_id || "")
|
|
104
|
+
)}
|
|
105
|
+
</span>
|
|
106
|
+
) : (
|
|
107
|
+
<span className="h-9 w-9 shrink-0" />
|
|
108
|
+
)}
|
|
109
|
+
<div className="flex-1 min-w-0 overflow-hidden">
|
|
110
|
+
{(showName ?? true) && (showAgent || displayName) && (
|
|
111
|
+
<p className="mb-1 text-xs text-muted-foreground truncate">{displayName}</p>
|
|
112
|
+
)}
|
|
113
|
+
<div
|
|
114
|
+
className={cn(
|
|
115
|
+
"rounded-3xl p-3 sm:p-4 max-w-full flex-1 basis-0 min-w-0 overflow-hidden",
|
|
116
|
+
"glass-surface-soft glass-noise",
|
|
117
|
+
isPending ? "ring-1 ring-ring/25" : "ring-1 ring-white/25"
|
|
118
|
+
)}
|
|
119
|
+
style={{ clipPath: "inset(0 round 1rem)", maxWidth: cardMaxWidth }}
|
|
120
|
+
>
|
|
121
|
+
<div className="text-sm wrap-anywhere overflow-hidden min-w-0">
|
|
122
|
+
<MarkdownRenderer>{request.prompt || ""}</MarkdownRenderer>
|
|
123
|
+
</div>
|
|
124
|
+
<PayloadCard
|
|
125
|
+
raw={request.payload}
|
|
126
|
+
disabled={disabled}
|
|
127
|
+
onPasteChoice={onPasteChoice}
|
|
128
|
+
onSubmitConfirm={(text, cancelled) =>
|
|
129
|
+
isPending ? onSubmitConfirm?.(request.request_id, text, cancelled) : undefined
|
|
130
|
+
}
|
|
131
|
+
selectedLines={selectedLines}
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
135
|
+
<span className="shrink-0">{formatFullTime(request.created_at || "")}</span>
|
|
136
|
+
{isPending && (
|
|
137
|
+
<>
|
|
138
|
+
<Badge variant="outline" className="text-xs shrink-0">
|
|
139
|
+
Waiting {getWaitingDuration(request.created_at || "")}
|
|
140
|
+
</Badge>
|
|
141
|
+
{!isPause && (
|
|
142
|
+
<>
|
|
143
|
+
<Badge variant="default" className="text-xs shrink-0">
|
|
144
|
+
Pending
|
|
145
|
+
</Badge>
|
|
146
|
+
{onReply && (
|
|
147
|
+
<Button
|
|
148
|
+
variant="link"
|
|
149
|
+
size="sm"
|
|
150
|
+
className="h-auto p-0 text-xs"
|
|
151
|
+
onClick={onReply}
|
|
152
|
+
disabled={disabled}
|
|
153
|
+
>
|
|
154
|
+
Reply
|
|
155
|
+
</Button>
|
|
156
|
+
)}
|
|
157
|
+
{onCancel && (
|
|
158
|
+
<Button
|
|
159
|
+
variant="link"
|
|
160
|
+
size="sm"
|
|
161
|
+
className="h-auto p-0 text-xs text-destructive"
|
|
162
|
+
onClick={onCancel}
|
|
163
|
+
disabled={disabled}
|
|
164
|
+
>
|
|
165
|
+
End
|
|
166
|
+
</Button>
|
|
167
|
+
)}
|
|
168
|
+
</>
|
|
169
|
+
)}
|
|
170
|
+
</>
|
|
171
|
+
)}
|
|
172
|
+
{request.status === "COMPLETED" && (
|
|
173
|
+
<Badge variant="secondary" className="text-xs shrink-0">
|
|
174
|
+
Replied
|
|
175
|
+
</Badge>
|
|
176
|
+
)}
|
|
177
|
+
{request.status === "CANCELLED" && (
|
|
178
|
+
<Badge variant="destructive" className="text-xs shrink-0">
|
|
179
|
+
Ended
|
|
180
|
+
</Badge>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|