@polpo-ai/dashboard 0.1.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/LICENSE +13 -0
- package/README.md +44 -0
- package/app/(dashboard)/error.tsx +47 -0
- package/app/(dashboard)/layout.tsx +41 -0
- package/app/(dashboard)/loading.tsx +30 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/memory/view.tsx +36 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/models-view.tsx +30 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/page.tsx +51 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/prompt/view.tsx +211 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/skills/view.tsx +84 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/tools/view.tsx +525 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/vault/view.tsx +61 -0
- package/app/(dashboard)/projects/[id]/agents/page.tsx +39 -0
- package/app/(dashboard)/projects/[id]/agents/view.tsx +283 -0
- package/app/(dashboard)/projects/[id]/layout.tsx +11 -0
- package/app/(dashboard)/projects/[id]/logs/page.tsx +27 -0
- package/app/(dashboard)/projects/[id]/logs/view.tsx +184 -0
- package/app/(dashboard)/projects/[id]/memory/page.tsx +40 -0
- package/app/(dashboard)/projects/[id]/memory/view.tsx +17 -0
- package/app/(dashboard)/projects/[id]/missions/[missionId]/page.tsx +88 -0
- package/app/(dashboard)/projects/[id]/missions/[missionId]/view.tsx +569 -0
- package/app/(dashboard)/projects/[id]/missions/page.tsx +32 -0
- package/app/(dashboard)/projects/[id]/missions/view.tsx +282 -0
- package/app/(dashboard)/projects/[id]/onboarding-checklist.tsx +359 -0
- package/app/(dashboard)/projects/[id]/page.tsx +68 -0
- package/app/(dashboard)/projects/[id]/playbooks/[name]/page.tsx +5 -0
- package/app/(dashboard)/projects/[id]/playbooks/[name]/view.tsx +433 -0
- package/app/(dashboard)/projects/[id]/playbooks/page.tsx +5 -0
- package/app/(dashboard)/projects/[id]/playbooks/view.tsx +222 -0
- package/app/(dashboard)/projects/[id]/schedules/page.tsx +44 -0
- package/app/(dashboard)/projects/[id]/schedules/view.tsx +385 -0
- package/app/(dashboard)/projects/[id]/sessions/[sessionId]/page.tsx +55 -0
- package/app/(dashboard)/projects/[id]/sessions/[sessionId]/raw-view.tsx +373 -0
- package/app/(dashboard)/projects/[id]/sessions/[sessionId]/view.tsx +124 -0
- package/app/(dashboard)/projects/[id]/sessions/page.tsx +31 -0
- package/app/(dashboard)/projects/[id]/sessions/view.tsx +242 -0
- package/app/(dashboard)/projects/[id]/settings/page.tsx +52 -0
- package/app/(dashboard)/projects/[id]/skills/[name]/page.tsx +65 -0
- package/app/(dashboard)/projects/[id]/skills/[name]/view.tsx +227 -0
- package/app/(dashboard)/projects/[id]/skills/page.tsx +31 -0
- package/app/(dashboard)/projects/[id]/skills/view.tsx +243 -0
- package/app/(dashboard)/projects/[id]/storage/page.tsx +5 -0
- package/app/(dashboard)/projects/[id]/storage/view.tsx +385 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/activity/view.tsx +336 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/assessment/view.tsx +113 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/blueprint.ts +72 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/output/view.tsx +90 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/page.tsx +63 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/view.tsx +137 -0
- package/app/(dashboard)/projects/[id]/tasks/page.tsx +40 -0
- package/app/(dashboard)/projects/[id]/tasks/view.tsx +421 -0
- package/app/(dashboard)/projects/[id]/view.tsx +421 -0
- package/app/(dashboard)/projects/[id]/welcome-banner.tsx +156 -0
- package/app/(dashboard)/projects/projects-list.tsx +126 -0
- package/app/(playground)/error.tsx +42 -0
- package/app/(playground)/layout.tsx +27 -0
- package/app/(playground)/projects/[id]/playground/page.tsx +57 -0
- package/app/(playground)/projects/[id]/playground/view.tsx +167 -0
- package/app/(playground)/projects/[id]/playground-legacy/page.tsx +47 -0
- package/app/(playground)/projects/[id]/playground-legacy/skeleton.tsx +63 -0
- package/app/(playground)/projects/[id]/playground-legacy/view.tsx +237 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +385 -0
- package/app/icon.svg +6 -0
- package/app/layout.tsx +39 -0
- package/app/page.tsx +11 -0
- package/components/ai-elements/code-block.tsx +562 -0
- package/components/dashboard/agent-capabilities.tsx +326 -0
- package/components/dashboard/agent-identity-header.tsx +50 -0
- package/components/dashboard/agent-model-picker.tsx +195 -0
- package/components/dashboard/agent-model-selector.tsx +121 -0
- package/components/dashboard/agent-picker-card.tsx +46 -0
- package/components/dashboard/agent-studio.tsx +242 -0
- package/components/dashboard/agents-table.tsx +48 -0
- package/components/dashboard/breadcrumb.tsx +70 -0
- package/components/dashboard/builder-chat.tsx +418 -0
- package/components/dashboard/chat-shell.tsx +40 -0
- package/components/dashboard/client-picker.tsx +63 -0
- package/components/dashboard/command-snippet.tsx +77 -0
- package/components/dashboard/connect-dialog.tsx +680 -0
- package/components/dashboard/copy-button.tsx +45 -0
- package/components/dashboard/copy-card.tsx +48 -0
- package/components/dashboard/file-browser.tsx +161 -0
- package/components/dashboard/hint.tsx +22 -0
- package/components/dashboard/inference-mode.tsx +15 -0
- package/components/dashboard/manual-refresh-button.tsx +52 -0
- package/components/dashboard/markdown.tsx +31 -0
- package/components/dashboard/mcp-install-panel.tsx +196 -0
- package/components/dashboard/mission-graph.tsx +262 -0
- package/components/dashboard/nav-tabs.tsx +86 -0
- package/components/dashboard/polpo-chat.tsx +468 -0
- package/components/dashboard/project-copilot.tsx +253 -0
- package/components/dashboard/sdk-snippet-panel.tsx +194 -0
- package/components/dashboard/section-header.tsx +37 -0
- package/components/dashboard/settings-form.tsx +2477 -0
- package/components/dashboard/sidebar.tsx +385 -0
- package/components/dashboard/skeletons.tsx +1054 -0
- package/components/dashboard/skills-install-wizard.tsx +273 -0
- package/components/dashboard/swarm-runs.tsx +316 -0
- package/components/dashboard/tab-toggle.tsx +35 -0
- package/components/dashboard/task-studio.tsx +224 -0
- package/components/dashboard/top-header.tsx +203 -0
- package/components/dashboard/webhook-deliveries.tsx +234 -0
- package/components/icons/coding-agents.tsx +151 -0
- package/components/json-ld.tsx +8 -0
- package/components/providers.tsx +25 -0
- package/components/ui/badge.tsx +52 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/card.tsx +103 -0
- package/components/ui/chart.tsx +356 -0
- package/components/ui/command.tsx +196 -0
- package/components/ui/dialog.tsx +157 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/multi-select.tsx +161 -0
- package/components/ui/popover.tsx +90 -0
- package/components/ui/select.tsx +201 -0
- package/components/ui/separator.tsx +25 -0
- package/components/ui/sheet.tsx +135 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/tabs.tsx +56 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/hooks/use-desktop-sidebar.tsx +50 -0
- package/hooks/use-mobile-sidebar.tsx +37 -0
- package/index.ts +14 -0
- package/lib/api.ts +194 -0
- package/lib/builder-context.ts +60 -0
- package/lib/data-client.ts +68 -0
- package/lib/get-query-client.ts +25 -0
- package/lib/polpo-client.tsx +49 -0
- package/lib/tool-catalog.ts +234 -0
- package/lib/use-event-catalog.ts +28 -0
- package/lib/utils.ts +6 -0
- package/package.json +99 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Suspense } from "react";
|
|
2
|
+
import { dataApi } from "#/lib/api";
|
|
3
|
+
import { SessionDetailSkeleton } from "#/components/dashboard/skeletons";
|
|
4
|
+
import SessionDetailView, {
|
|
5
|
+
type Attachment,
|
|
6
|
+
type Message,
|
|
7
|
+
type Session,
|
|
8
|
+
} from "./view";
|
|
9
|
+
|
|
10
|
+
async function SessionData({ id, sessionId }: { id: string; sessionId: string }) {
|
|
11
|
+
let session: Session | null = null;
|
|
12
|
+
let messages: Message[] = [];
|
|
13
|
+
let attachments: Attachment[] = [];
|
|
14
|
+
|
|
15
|
+
const [detailRes, attachmentsRes] = await Promise.allSettled([
|
|
16
|
+
dataApi<{ ok: boolean; data: { session: Session; messages: Message[] } }>(
|
|
17
|
+
id,
|
|
18
|
+
`/v1/chat/sessions/${sessionId}/messages`,
|
|
19
|
+
),
|
|
20
|
+
dataApi<{ ok: boolean; data: Attachment[] }>(
|
|
21
|
+
id,
|
|
22
|
+
`/v1/attachments?sessionId=${sessionId}`,
|
|
23
|
+
),
|
|
24
|
+
]);
|
|
25
|
+
if (detailRes.status === "fulfilled") {
|
|
26
|
+
session = detailRes.value.data?.session ?? null;
|
|
27
|
+
messages = detailRes.value.data?.messages ?? [];
|
|
28
|
+
}
|
|
29
|
+
if (attachmentsRes.status === "fulfilled") {
|
|
30
|
+
attachments = attachmentsRes.value.data ?? [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<SessionDetailView
|
|
35
|
+
projectId={id}
|
|
36
|
+
session={session}
|
|
37
|
+
messages={messages}
|
|
38
|
+
attachments={attachments}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default async function SessionDetailPage({
|
|
44
|
+
params,
|
|
45
|
+
}: {
|
|
46
|
+
params: Promise<{ id: string; sessionId: string }>;
|
|
47
|
+
}) {
|
|
48
|
+
const { id, sessionId } = await params;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Suspense fallback={<SessionDetailSkeleton />}>
|
|
52
|
+
<SessionData id={id} sessionId={sessionId} />
|
|
53
|
+
</Suspense>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Paperclip, FileText, Image as ImageIcon, FileSpreadsheet, Copy, Check } from "lucide-react";
|
|
5
|
+
import type { Attachment, ContentPart, Message } from "./view";
|
|
6
|
+
|
|
7
|
+
function formatBytes(bytes: number): string {
|
|
8
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
9
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
10
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function AttachmentIcon({ mimeType }: { mimeType: string }) {
|
|
14
|
+
if (mimeType.startsWith("image/")) return <ImageIcon className="h-3.5 w-3.5" />;
|
|
15
|
+
if (mimeType.includes("spreadsheet") || mimeType.includes("csv")) return <FileSpreadsheet className="h-3.5 w-3.5" />;
|
|
16
|
+
return <FileText className="h-3.5 w-3.5" />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Extract plain text from content (string or ContentPart[]) */
|
|
20
|
+
function extractText(content: string | ContentPart[]): string {
|
|
21
|
+
if (typeof content === "string") return content;
|
|
22
|
+
if (!Array.isArray(content)) return String(content ?? "");
|
|
23
|
+
return content
|
|
24
|
+
.filter((p) => p.type === "text")
|
|
25
|
+
.map((p) => p.text ?? "")
|
|
26
|
+
.join("\n");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Extract non-text parts (images, files) */
|
|
30
|
+
function extractParts(content: string | ContentPart[]): ContentPart[] {
|
|
31
|
+
if (typeof content === "string" || !Array.isArray(content)) return [];
|
|
32
|
+
return content.filter((p) => p.type !== "text");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function RawView({ messages, attachments, projectId }: { messages: Message[]; attachments: Attachment[]; projectId: string }) {
|
|
36
|
+
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
|
37
|
+
// Tracks which "<rowId>:<field>" key was most recently copied so the
|
|
38
|
+
// matching button can flash a checkmark for 1.5s. Single string keeps
|
|
39
|
+
// the state tiny vs. a Set + per-block timers.
|
|
40
|
+
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
|
41
|
+
|
|
42
|
+
const toggle = (id: string) => {
|
|
43
|
+
setExpanded((prev) => {
|
|
44
|
+
const next = new Set(prev);
|
|
45
|
+
if (next.has(id)) next.delete(id); else next.add(id);
|
|
46
|
+
return next;
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const copyValue = async (e: React.MouseEvent, value: string, key: string) => {
|
|
51
|
+
e.stopPropagation();
|
|
52
|
+
try {
|
|
53
|
+
await navigator.clipboard.writeText(value);
|
|
54
|
+
setCopiedKey(key);
|
|
55
|
+
setTimeout(() => setCopiedKey((k) => (k === key ? null : k)), 1500);
|
|
56
|
+
} catch {
|
|
57
|
+
// Clipboard API can fail in non-secure contexts; silently swallow.
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Flatten: messages + their tool calls as separate rows
|
|
62
|
+
interface RawRow {
|
|
63
|
+
id: string;
|
|
64
|
+
type: "message" | "tool_call";
|
|
65
|
+
role?: string;
|
|
66
|
+
ts: string;
|
|
67
|
+
name?: string;
|
|
68
|
+
content?: string;
|
|
69
|
+
contentParts?: ContentPart[];
|
|
70
|
+
state?: string;
|
|
71
|
+
args?: Record<string, unknown>;
|
|
72
|
+
argsStr?: string;
|
|
73
|
+
result?: string;
|
|
74
|
+
parentId?: string;
|
|
75
|
+
messageId?: string;
|
|
76
|
+
attachments?: Attachment[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rows: RawRow[] = [];
|
|
80
|
+
const firstUserIdx = messages.findIndex(m => m.role === "user");
|
|
81
|
+
|
|
82
|
+
for (let mi = 0; mi < messages.length; mi++) {
|
|
83
|
+
const msg = messages[mi];
|
|
84
|
+
// Collect attachments for this message
|
|
85
|
+
const msgAtts = attachments.filter(a => a.messageId === msg.id);
|
|
86
|
+
const unlinked = mi === firstUserIdx ? attachments.filter(a => !a.messageId) : [];
|
|
87
|
+
const allAtts = [...msgAtts, ...unlinked];
|
|
88
|
+
|
|
89
|
+
const textContent = extractText(msg.content);
|
|
90
|
+
const contentParts = extractParts(msg.content);
|
|
91
|
+
|
|
92
|
+
// An assistant message that starts with `Error:` is the way the
|
|
93
|
+
// runner surfaces a failed LLM call into the session transcript.
|
|
94
|
+
// Without flagging it as `state: "error"` here, the row renders
|
|
95
|
+
// with a neutral "—" badge and looks identical to a normal
|
|
96
|
+
// assistant reply — making it easy to scroll past. Tagging it
|
|
97
|
+
// turns on the red dot + error styling that tool errors already
|
|
98
|
+
// use, so a failed session is visually obvious.
|
|
99
|
+
const isAssistantError = msg.role === "assistant"
|
|
100
|
+
&& typeof textContent === "string"
|
|
101
|
+
&& /^\s*Error:\s/i.test(textContent);
|
|
102
|
+
|
|
103
|
+
rows.push({
|
|
104
|
+
id: msg.id,
|
|
105
|
+
type: "message",
|
|
106
|
+
role: msg.role,
|
|
107
|
+
ts: msg.ts,
|
|
108
|
+
content: textContent || undefined,
|
|
109
|
+
contentParts: contentParts.length > 0 ? contentParts : undefined,
|
|
110
|
+
state: isAssistantError ? "error" : undefined,
|
|
111
|
+
messageId: msg.id,
|
|
112
|
+
attachments: allAtts.length > 0 ? allAtts : undefined,
|
|
113
|
+
});
|
|
114
|
+
if (msg.toolCalls) {
|
|
115
|
+
for (let ti = 0; ti < msg.toolCalls.length; ti++) {
|
|
116
|
+
const tc = msg.toolCalls[ti];
|
|
117
|
+
rows.push({
|
|
118
|
+
id: tc.id || `${msg.id}-tc-${ti}`,
|
|
119
|
+
type: "tool_call",
|
|
120
|
+
ts: msg.ts,
|
|
121
|
+
name: tc.name,
|
|
122
|
+
state: tc.state || undefined,
|
|
123
|
+
args: tc.arguments,
|
|
124
|
+
argsStr: tc.arguments ? JSON.stringify(tc.arguments, null, 2) : undefined,
|
|
125
|
+
result: tc.result || undefined,
|
|
126
|
+
content: undefined,
|
|
127
|
+
parentId: msg.id,
|
|
128
|
+
messageId: msg.id,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className="border border-border overflow-hidden">
|
|
136
|
+
<table className="w-full text-xs">
|
|
137
|
+
<thead>
|
|
138
|
+
<tr className="border-b border-border bg-muted/40 text-muted-foreground">
|
|
139
|
+
<th className="px-3 py-2 text-left font-medium w-16">Type</th>
|
|
140
|
+
<th className="px-3 py-2 text-left font-medium w-28">Role / Tool</th>
|
|
141
|
+
<th className="px-3 py-2 text-left font-medium w-20">Time</th>
|
|
142
|
+
<th className="px-3 py-2 text-left font-medium w-20">State</th>
|
|
143
|
+
<th className="px-3 py-2 text-left font-medium w-48 hidden md:table-cell">Summary</th>
|
|
144
|
+
<th className="px-3 py-2 text-left font-medium">Content / Result</th>
|
|
145
|
+
<th className="px-3 py-2 text-left font-medium w-40">Attachments</th>
|
|
146
|
+
</tr>
|
|
147
|
+
</thead>
|
|
148
|
+
<tbody className="font-mono">
|
|
149
|
+
{rows.map((row) => {
|
|
150
|
+
const isExpanded = expanded.has(row.id);
|
|
151
|
+
const isToolCall = row.type === "tool_call";
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<tr
|
|
155
|
+
key={row.id}
|
|
156
|
+
onClick={() => toggle(row.id)}
|
|
157
|
+
className={`border-b border-border last:border-0 cursor-pointer transition-colors hover:bg-muted/20 ${
|
|
158
|
+
isToolCall ? "bg-muted/10" : ""
|
|
159
|
+
}`}
|
|
160
|
+
>
|
|
161
|
+
<td className="px-3 py-2 align-top">
|
|
162
|
+
<span className={`inline-block rounded px-1.5 py-0.5 text-[10px] font-medium ${
|
|
163
|
+
isToolCall
|
|
164
|
+
? "bg-purple-500/10 text-purple-400"
|
|
165
|
+
: row.role === "user"
|
|
166
|
+
? "bg-blue-500/10 text-blue-400"
|
|
167
|
+
: "bg-green-500/10 text-green-400"
|
|
168
|
+
}`}>
|
|
169
|
+
{isToolCall ? "tool" : "msg"}
|
|
170
|
+
</span>
|
|
171
|
+
</td>
|
|
172
|
+
<td className="px-3 py-2 align-top text-muted-foreground">
|
|
173
|
+
{isToolCall ? row.name : row.role}
|
|
174
|
+
</td>
|
|
175
|
+
<td className="px-3 py-2 align-top text-muted-foreground/60">
|
|
176
|
+
{new Date(row.ts).toLocaleTimeString("en-US", {
|
|
177
|
+
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
|
|
178
|
+
})}
|
|
179
|
+
</td>
|
|
180
|
+
<td className="px-3 py-2 align-top">
|
|
181
|
+
{row.state ? (
|
|
182
|
+
<span className={`inline-flex items-center gap-1 text-[10px] ${
|
|
183
|
+
row.state === "completed" ? "text-green-500" : row.state === "error" ? "text-red-400" : "text-yellow-500"
|
|
184
|
+
}`}>
|
|
185
|
+
<span className={`h-1.5 w-1.5 rounded-full ${
|
|
186
|
+
row.state === "completed" ? "bg-green-500" : row.state === "error" ? "bg-red-500" : "bg-yellow-500"
|
|
187
|
+
}`} />
|
|
188
|
+
{row.state}
|
|
189
|
+
</span>
|
|
190
|
+
) : (
|
|
191
|
+
<span className="text-muted-foreground/30">—</span>
|
|
192
|
+
)}
|
|
193
|
+
</td>
|
|
194
|
+
{/* Summary — key args or content preview, always visible */}
|
|
195
|
+
<td className="px-3 py-2 align-top text-muted-foreground/70 hidden md:table-cell">
|
|
196
|
+
{isToolCall && row.args ? (
|
|
197
|
+
<span className="truncate block">
|
|
198
|
+
{(row.args as Record<string, unknown>).path as string
|
|
199
|
+
|| (row.args as Record<string, unknown>).command as string
|
|
200
|
+
|| (row.args as Record<string, unknown>).pattern as string
|
|
201
|
+
|| (row.args as Record<string, unknown>).url as string
|
|
202
|
+
|| (row.args as Record<string, unknown>).content
|
|
203
|
+
? `${String(Object.values(row.args)[0]).substring(0, 40)}`
|
|
204
|
+
: Object.keys(row.args).join(", ")
|
|
205
|
+
}
|
|
206
|
+
</span>
|
|
207
|
+
) : !isToolCall && row.content ? (
|
|
208
|
+
<span className="truncate block">
|
|
209
|
+
{row.content.substring(0, 50)}{row.content.length > 50 ? "…" : ""}
|
|
210
|
+
</span>
|
|
211
|
+
) : (
|
|
212
|
+
<span className="text-muted-foreground/20">—</span>
|
|
213
|
+
)}
|
|
214
|
+
</td>
|
|
215
|
+
{/* Content / Result — expandable */}
|
|
216
|
+
<td className="px-3 py-2 align-top">
|
|
217
|
+
{isExpanded ? (
|
|
218
|
+
<div className="space-y-3 pb-2">
|
|
219
|
+
{/* ID — click to copy */}
|
|
220
|
+
<div className="flex gap-2 text-[10px]">
|
|
221
|
+
<span className="text-muted-foreground/40 uppercase w-12 shrink-0">id</span>
|
|
222
|
+
<button
|
|
223
|
+
type="button"
|
|
224
|
+
onClick={(e) => copyValue(e, row.id, `${row.id}:id`)}
|
|
225
|
+
className="group inline-flex items-center gap-1.5 text-muted-foreground/60 hover:text-foreground break-all text-left"
|
|
226
|
+
title="Click to copy id"
|
|
227
|
+
>
|
|
228
|
+
<span className="break-all">{row.id}</span>
|
|
229
|
+
{copiedKey === `${row.id}:id` ? (
|
|
230
|
+
<Check className="h-3 w-3 text-emerald-500 shrink-0" />
|
|
231
|
+
) : (
|
|
232
|
+
<Copy className="h-3 w-3 opacity-0 group-hover:opacity-60 transition-opacity shrink-0" />
|
|
233
|
+
)}
|
|
234
|
+
</button>
|
|
235
|
+
</div>
|
|
236
|
+
{/* Content (messages) — red on error, hover-copy button */}
|
|
237
|
+
{row.content && (
|
|
238
|
+
<div className="group/block relative">
|
|
239
|
+
<span className={`text-[10px] uppercase tracking-wider ${row.state === "error" ? "text-red-400" : "text-muted-foreground"}`}>
|
|
240
|
+
{row.state === "error" ? "error" : "content"}
|
|
241
|
+
</span>
|
|
242
|
+
<button
|
|
243
|
+
type="button"
|
|
244
|
+
onClick={(e) => copyValue(e, row.content!, `${row.id}:content`)}
|
|
245
|
+
className="absolute right-1 top-3 inline-flex items-center gap-1 rounded bg-background/80 px-1.5 py-0.5 text-[10px] text-muted-foreground opacity-0 group-hover/block:opacity-100 hover:text-foreground transition-opacity"
|
|
246
|
+
title="Copy content"
|
|
247
|
+
>
|
|
248
|
+
{copiedKey === `${row.id}:content` ? (
|
|
249
|
+
<><Check className="h-3 w-3 text-emerald-500" /> copied</>
|
|
250
|
+
) : (
|
|
251
|
+
<><Copy className="h-3 w-3" /> copy</>
|
|
252
|
+
)}
|
|
253
|
+
</button>
|
|
254
|
+
<pre className={`mt-1 whitespace-pre-wrap break-words rounded bg-muted/50 p-2 max-h-60 overflow-auto ${
|
|
255
|
+
row.state === "error" ? "text-red-400" : "text-foreground"
|
|
256
|
+
}`}>{row.content}</pre>
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
{/* File/image parts */}
|
|
260
|
+
{row.contentParts && row.contentParts.length > 0 && (
|
|
261
|
+
<div>
|
|
262
|
+
<span className="text-[10px] text-muted-foreground uppercase tracking-wider">parts</span>
|
|
263
|
+
<div className="mt-1 space-y-1">
|
|
264
|
+
{row.contentParts.map((part, pi) => (
|
|
265
|
+
<div key={pi} className="flex items-center gap-2 rounded bg-muted/50 px-2 py-1.5">
|
|
266
|
+
{part.type === "image_url" && (
|
|
267
|
+
<>
|
|
268
|
+
<ImageIcon className="h-3 w-3 text-blue-400" />
|
|
269
|
+
<a href={part.image_url?.url} target="_blank" rel="noopener noreferrer" className="text-blue-400 hover:underline truncate">{part.image_url?.url}</a>
|
|
270
|
+
</>
|
|
271
|
+
)}
|
|
272
|
+
{part.type === "file" && (
|
|
273
|
+
<>
|
|
274
|
+
<FileText className="h-3 w-3 text-amber-400" />
|
|
275
|
+
<span className="font-mono text-amber-400">{part.file?.filename ?? part.file?.path}</span>
|
|
276
|
+
{part.file?.mimeType && <span className="text-muted-foreground/40">{part.file.mimeType}</span>}
|
|
277
|
+
</>
|
|
278
|
+
)}
|
|
279
|
+
</div>
|
|
280
|
+
))}
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
{/* Args (tool calls) — hover-copy */}
|
|
285
|
+
{row.argsStr && (
|
|
286
|
+
<div className="group/block relative">
|
|
287
|
+
<span className="text-[10px] text-muted-foreground uppercase tracking-wider">arguments</span>
|
|
288
|
+
<button
|
|
289
|
+
type="button"
|
|
290
|
+
onClick={(e) => copyValue(e, row.argsStr!, `${row.id}:args`)}
|
|
291
|
+
className="absolute right-1 top-3 inline-flex items-center gap-1 rounded bg-background/80 px-1.5 py-0.5 text-[10px] text-muted-foreground opacity-0 group-hover/block:opacity-100 hover:text-foreground transition-opacity"
|
|
292
|
+
title="Copy arguments"
|
|
293
|
+
>
|
|
294
|
+
{copiedKey === `${row.id}:args` ? (
|
|
295
|
+
<><Check className="h-3 w-3 text-emerald-500" /> copied</>
|
|
296
|
+
) : (
|
|
297
|
+
<><Copy className="h-3 w-3" /> copy</>
|
|
298
|
+
)}
|
|
299
|
+
</button>
|
|
300
|
+
<pre className="mt-1 whitespace-pre-wrap break-words rounded bg-muted/50 p-2 text-muted-foreground max-h-40 overflow-auto">{row.argsStr}</pre>
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
{/* Result (tool calls) — hover-copy */}
|
|
304
|
+
{row.result && (
|
|
305
|
+
<div className="group/block relative">
|
|
306
|
+
<span className={`text-[10px] uppercase tracking-wider ${row.state === "error" ? "text-red-400" : "text-muted-foreground"}`}>
|
|
307
|
+
{row.state === "error" ? "error" : "result"}
|
|
308
|
+
</span>
|
|
309
|
+
<button
|
|
310
|
+
type="button"
|
|
311
|
+
onClick={(e) => copyValue(e, row.result!, `${row.id}:result`)}
|
|
312
|
+
className="absolute right-1 top-3 inline-flex items-center gap-1 rounded bg-background/80 px-1.5 py-0.5 text-[10px] text-muted-foreground opacity-0 group-hover/block:opacity-100 hover:text-foreground transition-opacity"
|
|
313
|
+
title="Copy result"
|
|
314
|
+
>
|
|
315
|
+
{copiedKey === `${row.id}:result` ? (
|
|
316
|
+
<><Check className="h-3 w-3 text-emerald-500" /> copied</>
|
|
317
|
+
) : (
|
|
318
|
+
<><Copy className="h-3 w-3" /> copy</>
|
|
319
|
+
)}
|
|
320
|
+
</button>
|
|
321
|
+
<pre className={`mt-1 whitespace-pre-wrap break-words rounded bg-muted/50 p-2 max-h-60 overflow-auto ${
|
|
322
|
+
row.state === "error" ? "text-red-400" : "text-muted-foreground"
|
|
323
|
+
}`}>{row.result}</pre>
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
326
|
+
{/* No data */}
|
|
327
|
+
{!row.content && !row.argsStr && !row.result && (
|
|
328
|
+
<span className="text-muted-foreground/40 italic">No data</span>
|
|
329
|
+
)}
|
|
330
|
+
</div>
|
|
331
|
+
) : (
|
|
332
|
+
<span className="text-muted-foreground truncate block">
|
|
333
|
+
{isToolCall
|
|
334
|
+
? row.result
|
|
335
|
+
? row.result.substring(0, 80) + (row.result.length > 80 ? "…" : "")
|
|
336
|
+
: (<span className="text-muted-foreground/30 italic">no result</span>)
|
|
337
|
+
: row.content
|
|
338
|
+
? row.content.substring(0, 100) + (row.content.length > 100 ? "…" : "")
|
|
339
|
+
: (<span className="text-muted-foreground/30 italic">empty</span>)
|
|
340
|
+
}
|
|
341
|
+
</span>
|
|
342
|
+
)}
|
|
343
|
+
</td>
|
|
344
|
+
<td className="px-3 py-2 align-top">
|
|
345
|
+
{row.attachments && row.attachments.length > 0 ? (
|
|
346
|
+
<div className="flex flex-col gap-1">
|
|
347
|
+
{row.attachments.map(att => (
|
|
348
|
+
<a
|
|
349
|
+
key={att.id}
|
|
350
|
+
href={`${process.env.NEXT_PUBLIC_API_URL}/v1/projects/${projectId}/data/v1/attachments/${att.id}/download`}
|
|
351
|
+
target="_blank"
|
|
352
|
+
rel="noopener noreferrer"
|
|
353
|
+
onClick={(e) => e.stopPropagation()}
|
|
354
|
+
className="inline-flex items-center gap-1 text-[10px] text-amber-400 hover:text-amber-300 transition-colors"
|
|
355
|
+
>
|
|
356
|
+
<Paperclip className="h-2.5 w-2.5" />
|
|
357
|
+
<span className="font-mono">{att.filename}</span>
|
|
358
|
+
<span className="text-muted-foreground/40">{formatBytes(att.size)}</span>
|
|
359
|
+
</a>
|
|
360
|
+
))}
|
|
361
|
+
</div>
|
|
362
|
+
) : (
|
|
363
|
+
<span className="text-muted-foreground/20">—</span>
|
|
364
|
+
)}
|
|
365
|
+
</td>
|
|
366
|
+
</tr>
|
|
367
|
+
);
|
|
368
|
+
})}
|
|
369
|
+
</tbody>
|
|
370
|
+
</table>
|
|
371
|
+
</div>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { ArrowLeft, ExternalLink } from "lucide-react";
|
|
3
|
+
import { ManualRefreshButton } from "#/components/dashboard/manual-refresh-button";
|
|
4
|
+
import RawView from "./raw-view";
|
|
5
|
+
|
|
6
|
+
export interface Attachment {
|
|
7
|
+
id: string;
|
|
8
|
+
sessionId: string;
|
|
9
|
+
messageId?: string;
|
|
10
|
+
filename: string;
|
|
11
|
+
mimeType: string;
|
|
12
|
+
size: number;
|
|
13
|
+
path: string;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ToolCallInfo {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
arguments?: Record<string, unknown>;
|
|
21
|
+
result?: string;
|
|
22
|
+
state: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ContentPart {
|
|
26
|
+
type: "text" | "image_url" | "file";
|
|
27
|
+
text?: string;
|
|
28
|
+
image_url?: { url: string };
|
|
29
|
+
file?: { path: string; filename?: string; mimeType?: string };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Message {
|
|
33
|
+
id: string;
|
|
34
|
+
role: "user" | "assistant";
|
|
35
|
+
content: string | ContentPart[];
|
|
36
|
+
ts: string;
|
|
37
|
+
toolCalls?: ToolCallInfo[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Session {
|
|
41
|
+
id: string;
|
|
42
|
+
title?: string;
|
|
43
|
+
agent?: string;
|
|
44
|
+
createdAt: string;
|
|
45
|
+
messageCount: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default function SessionDetailView({
|
|
49
|
+
projectId,
|
|
50
|
+
session,
|
|
51
|
+
messages,
|
|
52
|
+
attachments,
|
|
53
|
+
}: {
|
|
54
|
+
projectId: string;
|
|
55
|
+
session: Session | null;
|
|
56
|
+
messages: Message[];
|
|
57
|
+
attachments: Attachment[];
|
|
58
|
+
}) {
|
|
59
|
+
if (!session) {
|
|
60
|
+
return (
|
|
61
|
+
<div data-testid="session-not-found" className="mt-4 border border-border p-8 text-center text-sm text-muted-foreground">
|
|
62
|
+
Session not found.
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div>
|
|
69
|
+
{/* Back link */}
|
|
70
|
+
<Link
|
|
71
|
+
href={`/projects/${projectId}/sessions`}
|
|
72
|
+
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
73
|
+
>
|
|
74
|
+
<ArrowLeft className="h-3 w-3" />
|
|
75
|
+
Sessions
|
|
76
|
+
</Link>
|
|
77
|
+
|
|
78
|
+
{/* Header */}
|
|
79
|
+
<div className="mt-4 flex items-start justify-between gap-4">
|
|
80
|
+
<div className="min-w-0">
|
|
81
|
+
<h2 className="text-lg font-semibold tracking-tight truncate">
|
|
82
|
+
{session.title ?? "Untitled session"}
|
|
83
|
+
</h2>
|
|
84
|
+
<div className="mt-1 flex items-center gap-3 text-xs text-muted-foreground flex-wrap">
|
|
85
|
+
<span className="font-mono">{session.id}</span>
|
|
86
|
+
{session.agent && (
|
|
87
|
+
<span className="rounded bg-secondary px-1.5 py-0.5 text-[10px] font-mono">
|
|
88
|
+
{session.agent}
|
|
89
|
+
</span>
|
|
90
|
+
)}
|
|
91
|
+
<span>{new Date(session.createdAt).toLocaleString()}</span>
|
|
92
|
+
<span>{messages.length} messages</span>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
<div className="flex shrink-0 items-center gap-3">
|
|
96
|
+
<ManualRefreshButton />
|
|
97
|
+
{/* Resume in playground — opens a new tab with ?session=<id>.
|
|
98
|
+
Lets the user continue an existing conversation interactively
|
|
99
|
+
from the read-only session detail. */}
|
|
100
|
+
<Link
|
|
101
|
+
href={`/projects/${projectId}/playground?session=${session.id}`}
|
|
102
|
+
target="_blank"
|
|
103
|
+
rel="noopener noreferrer"
|
|
104
|
+
className="inline-flex items-center gap-1.5 rounded border border-border px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-foreground/30 hover:text-foreground"
|
|
105
|
+
>
|
|
106
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
107
|
+
Open in playground
|
|
108
|
+
</Link>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* Content */}
|
|
113
|
+
{messages.length > 0 ? (
|
|
114
|
+
<div data-testid="session-messages" className="mt-5">
|
|
115
|
+
<RawView messages={messages} attachments={attachments} projectId={projectId} />
|
|
116
|
+
</div>
|
|
117
|
+
) : (
|
|
118
|
+
<div data-testid="session-messages-empty" className="mt-6 border border-border p-8 text-center text-sm text-muted-foreground">
|
|
119
|
+
No messages in this session.
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Suspense } from "react";
|
|
2
|
+
import { dataApi } from "#/lib/api";
|
|
3
|
+
import { SessionsListSkeleton } from "#/components/dashboard/skeletons";
|
|
4
|
+
import SessionsView, { type Session } from "./view";
|
|
5
|
+
|
|
6
|
+
async function SessionsData({ id }: { id: string }) {
|
|
7
|
+
let sessions: Session[] = [];
|
|
8
|
+
try {
|
|
9
|
+
const res = await dataApi<{ ok: boolean; data: { sessions: Session[] } }>(
|
|
10
|
+
id,
|
|
11
|
+
"/v1/chat/sessions",
|
|
12
|
+
);
|
|
13
|
+
sessions = res.data?.sessions ?? [];
|
|
14
|
+
} catch {}
|
|
15
|
+
|
|
16
|
+
return <SessionsView initialSessions={sessions} />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default async function SessionsPage({
|
|
20
|
+
params,
|
|
21
|
+
}: {
|
|
22
|
+
params: Promise<{ id: string }>;
|
|
23
|
+
}) {
|
|
24
|
+
const { id } = await params;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Suspense fallback={<SessionsListSkeleton />}>
|
|
28
|
+
<SessionsData id={id} />
|
|
29
|
+
</Suspense>
|
|
30
|
+
);
|
|
31
|
+
}
|