@sentry/junior-dashboard 0.61.0 → 0.63.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/dist/app.d.ts +1 -0
- package/dist/app.js +44 -37
- package/dist/assets.d.ts +2 -0
- package/dist/client/api.d.ts +1 -1
- package/dist/client/components/Metric.d.ts +22 -0
- package/dist/client/components/TelemetryMetrics.d.ts +24 -0
- package/dist/client/components/TranscriptText.d.ts +1 -0
- package/dist/client/components/transcriptRenderModel.d.ts +24 -8
- package/dist/client/format.d.ts +65 -13
- package/dist/client/toolInvocations.d.ts +7 -0
- package/dist/client/types.d.ts +17 -100
- package/dist/client.js +59 -55992
- package/dist/handler.js +44 -37
- package/dist/index.d.ts +8 -0
- package/dist/index.js +564 -0
- package/dist/nitro.d.ts +6 -1
- package/dist/tailwind.css +1 -1
- package/dist/url.d.ts +13 -0
- package/package.json +8 -5
- package/src/app.ts +24 -16
- package/src/assets.ts +2 -0
- package/src/auth.ts +2 -35
- package/src/client/components/ConversationRowStats.tsx +3 -2
- package/src/client/components/Metric.tsx +159 -0
- package/src/client/components/TelemetryMetrics.tsx +124 -0
- package/src/client/components/ToolFrame.tsx +3 -3
- package/src/client/components/TranscriptText.tsx +5 -2
- package/src/client/components/TranscriptToolView.tsx +9 -7
- package/src/client/components/TranscriptTurn.tsx +435 -84
- package/src/client/components/TurnDurationChart.tsx +229 -78
- package/src/client/components/transcriptRenderModel.ts +66 -22
- package/src/client/format.ts +369 -103
- package/src/client/pages/ConversationPage.tsx +77 -38
- package/src/client/toolInvocations.ts +16 -0
- package/src/client/types.ts +34 -90
- package/src/index.ts +74 -0
- package/src/nitro.ts +6 -1
- package/src/url.ts +68 -0
|
@@ -4,16 +4,24 @@ import { useConversationData } from "../api";
|
|
|
4
4
|
import { StatusBadge } from "../components/StatusBadge";
|
|
5
5
|
import {
|
|
6
6
|
buildConversations,
|
|
7
|
+
conversationIdentityMeta,
|
|
7
8
|
conversationDisplayTitle,
|
|
8
9
|
formatConversationDuration,
|
|
9
10
|
formatRelativeTime,
|
|
10
11
|
formatTime,
|
|
11
|
-
formatUsageTotal,
|
|
12
12
|
slackLocationLabel,
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
summarizeMessages,
|
|
14
|
+
summarizeToolCalls,
|
|
15
|
+
summarizeUsage,
|
|
15
16
|
visualStatusForConversation,
|
|
16
17
|
} from "../format";
|
|
18
|
+
import { MetricList, type MetricListItem } from "../components/Metric";
|
|
19
|
+
import {
|
|
20
|
+
DurationMetric,
|
|
21
|
+
MessagesMetric,
|
|
22
|
+
TokenMetric,
|
|
23
|
+
ToolCallsMetric,
|
|
24
|
+
} from "../components/TelemetryMetrics";
|
|
17
25
|
import { Transcript } from "../components/Transcript";
|
|
18
26
|
import { TranscriptLoading } from "../components/TranscriptLoading";
|
|
19
27
|
import type {
|
|
@@ -81,15 +89,9 @@ function ConversationIdentity(props: {
|
|
|
81
89
|
conversation: Conversation | undefined;
|
|
82
90
|
conversationId: string | undefined;
|
|
83
91
|
}) {
|
|
84
|
-
const id = props.conversationId ?? "missing conversation id";
|
|
85
|
-
const owner =
|
|
86
|
-
props.conversation?.requesterIdentity?.email ??
|
|
87
|
-
props.conversation?.requester ??
|
|
88
|
-
props.conversation?.requesterIdentity?.slackUserName;
|
|
89
92
|
return (
|
|
90
93
|
<>
|
|
91
|
-
{
|
|
92
|
-
{id}
|
|
94
|
+
{conversationIdentityMeta(props.conversation, props.conversationId)}
|
|
93
95
|
{props.conversation?.sentryConversationUrl ? (
|
|
94
96
|
<>
|
|
95
97
|
{" · "}
|
|
@@ -112,41 +114,78 @@ function ConversationStats(props: {
|
|
|
112
114
|
detail?: ConversationDetailFeed;
|
|
113
115
|
}) {
|
|
114
116
|
if (!props.conversation) return null;
|
|
115
|
-
const
|
|
116
|
-
? props.detail.turns
|
|
117
|
-
(count, turn) => count + turnMessageCount(turn),
|
|
118
|
-
0,
|
|
119
|
-
)
|
|
117
|
+
const messageSummary = props.detail
|
|
118
|
+
? summarizeMessages(props.detail.turns)
|
|
120
119
|
: undefined;
|
|
121
|
-
const
|
|
122
|
-
? props.detail.turns
|
|
123
|
-
(count, turn) => count + turnToolCallCount(turn),
|
|
124
|
-
0,
|
|
125
|
-
)
|
|
120
|
+
const toolSummary = props.detail
|
|
121
|
+
? summarizeToolCalls(props.detail.turns)
|
|
126
122
|
: undefined;
|
|
127
|
-
const
|
|
123
|
+
const tokenSummary = summarizeUsage(
|
|
128
124
|
(props.detail?.turns ?? props.conversation.turns).map(
|
|
129
125
|
(turn) => turn.cumulativeUsage,
|
|
130
126
|
),
|
|
131
127
|
);
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
128
|
+
const location = slackLocationLabel(props.conversation, {
|
|
129
|
+
includeId: false,
|
|
130
|
+
});
|
|
131
|
+
const durationLabel = formatConversationDuration(props.conversation);
|
|
132
|
+
const turnCount = props.conversation.turns.length;
|
|
133
|
+
const rawStats: Array<MetricListItem | undefined> = [
|
|
134
|
+
location
|
|
135
|
+
? {
|
|
136
|
+
content: location,
|
|
137
|
+
key: "location",
|
|
138
|
+
}
|
|
139
|
+
: undefined,
|
|
140
|
+
{
|
|
141
|
+
content: `${turnCount} ${turnCount === 1 ? "turn" : "turns"}`,
|
|
142
|
+
key: "turns",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
content: (
|
|
146
|
+
<MessagesMetric loading={!props.detail} summary={messageSummary} />
|
|
147
|
+
),
|
|
148
|
+
key: "messages",
|
|
149
|
+
},
|
|
150
|
+
!props.detail || (toolSummary && toolSummary.total > 0)
|
|
151
|
+
? {
|
|
152
|
+
content: (
|
|
153
|
+
<ToolCallsMetric loading={!props.detail} summary={toolSummary} />
|
|
154
|
+
),
|
|
155
|
+
key: "tools",
|
|
156
|
+
}
|
|
157
|
+
: undefined,
|
|
158
|
+
tokenSummary
|
|
159
|
+
? {
|
|
160
|
+
content: <TokenMetric summary={tokenSummary} />,
|
|
161
|
+
key: "tokens",
|
|
162
|
+
}
|
|
163
|
+
: undefined,
|
|
164
|
+
durationLabel !== "none"
|
|
165
|
+
? {
|
|
166
|
+
content: (
|
|
167
|
+
<DurationMetric
|
|
168
|
+
endedAt={props.conversation.lastSeenAt}
|
|
169
|
+
label={durationLabel}
|
|
170
|
+
startedAt={props.conversation.startedAt}
|
|
171
|
+
/>
|
|
172
|
+
),
|
|
173
|
+
key: "duration",
|
|
174
|
+
}
|
|
175
|
+
: undefined,
|
|
176
|
+
{
|
|
177
|
+
content: `started ${formatTime(props.conversation.startedAt)}`,
|
|
178
|
+
key: "started",
|
|
179
|
+
},
|
|
180
|
+
];
|
|
181
|
+
const stats = rawStats.filter(
|
|
182
|
+
(item): item is MetricListItem => item !== undefined,
|
|
183
|
+
);
|
|
141
184
|
|
|
142
185
|
return (
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
{value}
|
|
148
|
-
</span>
|
|
149
|
-
))}
|
|
150
|
-
</div>
|
|
186
|
+
<MetricList
|
|
187
|
+
className="col-span-full break-words text-[0.76rem] leading-[1.45] text-[#888]"
|
|
188
|
+
items={stats}
|
|
189
|
+
/>
|
|
151
190
|
);
|
|
152
191
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type ToolInvocationRef = {
|
|
2
|
+
id?: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
/** Match tool call/result refs without inferring relationships from missing metadata. */
|
|
7
|
+
export function sameToolInvocation(
|
|
8
|
+
left: ToolInvocationRef,
|
|
9
|
+
right: ToolInvocationRef,
|
|
10
|
+
): boolean {
|
|
11
|
+
if (left.id || right.id) {
|
|
12
|
+
return Boolean(left.id && right.id && left.id === right.id);
|
|
13
|
+
}
|
|
14
|
+
if (left.name && right.name) return left.name === right.name;
|
|
15
|
+
return false;
|
|
16
|
+
}
|
package/src/client/types.ts
CHANGED
|
@@ -1,119 +1,59 @@
|
|
|
1
1
|
import type { BundledLanguage } from "shiki/bundle/web";
|
|
2
|
+
import type {
|
|
3
|
+
DashboardConversationReport,
|
|
4
|
+
DashboardRequesterIdentity,
|
|
5
|
+
DashboardSessionFeed,
|
|
6
|
+
DashboardSessionReport,
|
|
7
|
+
DashboardTurnReport,
|
|
8
|
+
DashboardTurnUsage,
|
|
9
|
+
HealthReport,
|
|
10
|
+
PluginReport,
|
|
11
|
+
RuntimeInfoReport,
|
|
12
|
+
SkillReport,
|
|
13
|
+
} from "@sentry/junior/reporting";
|
|
2
14
|
|
|
3
|
-
export type Health =
|
|
15
|
+
export type Health = HealthReport;
|
|
4
16
|
|
|
5
|
-
export type Runtime =
|
|
6
|
-
cwd: string;
|
|
7
|
-
descriptionText?: string;
|
|
8
|
-
homeDir: string;
|
|
9
|
-
packagedContent: { packageNames: string[] };
|
|
10
|
-
};
|
|
17
|
+
export type Runtime = RuntimeInfoReport;
|
|
11
18
|
|
|
12
|
-
export type Plugin =
|
|
19
|
+
export type Plugin = PluginReport;
|
|
13
20
|
|
|
14
|
-
export type Skill =
|
|
21
|
+
export type Skill = SkillReport;
|
|
15
22
|
|
|
16
|
-
export type RequesterIdentity =
|
|
17
|
-
email?: string;
|
|
18
|
-
fullName?: string;
|
|
19
|
-
slackUserId?: string;
|
|
20
|
-
slackUserName?: string;
|
|
21
|
-
};
|
|
23
|
+
export type RequesterIdentity = DashboardRequesterIdentity;
|
|
22
24
|
|
|
23
|
-
export type TurnUsage =
|
|
24
|
-
cachedInputTokens?: number;
|
|
25
|
-
cacheCreationTokens?: number;
|
|
26
|
-
inputTokens?: number;
|
|
27
|
-
outputTokens?: number;
|
|
28
|
-
totalTokens?: number;
|
|
29
|
-
};
|
|
25
|
+
export type TurnUsage = DashboardTurnUsage;
|
|
30
26
|
|
|
31
|
-
export type Session =
|
|
32
|
-
channel?: string;
|
|
33
|
-
channelName?: string;
|
|
34
|
-
conversationId?: string;
|
|
35
|
-
conversationTitle?: string;
|
|
36
|
-
cumulativeDurationMs?: number;
|
|
37
|
-
cumulativeUsage?: TurnUsage;
|
|
38
|
-
id: string;
|
|
39
|
-
lastProgressAt?: string;
|
|
40
|
-
lastSeenAt?: string;
|
|
41
|
-
requester?: string;
|
|
42
|
-
requesterIdentity?: RequesterIdentity;
|
|
43
|
-
sentryConversationUrl?: string;
|
|
44
|
-
sentryTraceUrl?: string;
|
|
45
|
-
startedAt?: string;
|
|
46
|
-
status: string;
|
|
47
|
-
surface?: string;
|
|
48
|
-
title?: string;
|
|
49
|
-
traceId?: string;
|
|
50
|
-
};
|
|
27
|
+
export type Session = DashboardSessionReport;
|
|
51
28
|
|
|
52
|
-
export type TranscriptPart =
|
|
53
|
-
|
|
54
|
-
chars?: number;
|
|
55
|
-
id?: string;
|
|
56
|
-
input?: unknown;
|
|
57
|
-
inputKeys?: string[];
|
|
58
|
-
inputSizeBytes?: number;
|
|
59
|
-
inputSizeChars?: number;
|
|
60
|
-
inputType?: string;
|
|
61
|
-
name?: string;
|
|
62
|
-
output?: unknown;
|
|
63
|
-
outputKeys?: string[];
|
|
64
|
-
outputSizeBytes?: number;
|
|
65
|
-
outputSizeChars?: number;
|
|
66
|
-
outputType?: string;
|
|
67
|
-
redacted?: boolean;
|
|
68
|
-
text?: string;
|
|
69
|
-
type: string;
|
|
70
|
-
};
|
|
29
|
+
export type TranscriptPart =
|
|
30
|
+
DashboardTurnReport["transcript"][number]["parts"][number];
|
|
71
31
|
|
|
72
|
-
export type TranscriptMessage =
|
|
73
|
-
parts: TranscriptPart[];
|
|
74
|
-
role: string;
|
|
75
|
-
timestamp?: number;
|
|
76
|
-
};
|
|
32
|
+
export type TranscriptMessage = DashboardTurnReport["transcript"][number];
|
|
77
33
|
|
|
78
|
-
export type ConversationTurn =
|
|
79
|
-
transcript: TranscriptMessage[];
|
|
80
|
-
transcriptAvailable: boolean;
|
|
81
|
-
transcriptMetadata?: TranscriptMessage[];
|
|
82
|
-
transcriptMessageCount?: number;
|
|
83
|
-
transcriptRedacted?: boolean;
|
|
84
|
-
transcriptRedactionReason?: "non_public_conversation";
|
|
85
|
-
};
|
|
34
|
+
export type ConversationTurn = DashboardTurnReport;
|
|
86
35
|
|
|
87
|
-
export type ConversationDetailFeed =
|
|
88
|
-
conversationId: string;
|
|
89
|
-
generatedAt: string;
|
|
90
|
-
turns: ConversationTurn[];
|
|
91
|
-
};
|
|
36
|
+
export type ConversationDetailFeed = DashboardConversationReport;
|
|
92
37
|
|
|
93
38
|
export type Conversation = {
|
|
94
39
|
channel?: string;
|
|
95
40
|
channelName?: string;
|
|
96
41
|
conversationTitle?: string;
|
|
97
42
|
id: string;
|
|
98
|
-
lastProgressAt
|
|
99
|
-
lastSeenAt
|
|
100
|
-
requester?: string;
|
|
43
|
+
lastProgressAt: string;
|
|
44
|
+
lastSeenAt: string;
|
|
101
45
|
requesterIdentity?: RequesterIdentity;
|
|
102
46
|
sentryConversationUrl?: string;
|
|
103
47
|
sentryTraceUrl?: string;
|
|
104
|
-
startedAt
|
|
48
|
+
startedAt: string;
|
|
105
49
|
status: Session["status"];
|
|
106
|
-
surface
|
|
50
|
+
surface: Session["surface"];
|
|
107
51
|
title: string;
|
|
108
52
|
traceId?: string;
|
|
109
53
|
turns: Session[];
|
|
110
54
|
};
|
|
111
55
|
|
|
112
|
-
export type SessionFeed =
|
|
113
|
-
generatedAt?: string;
|
|
114
|
-
sessions: Session[];
|
|
115
|
-
source: string;
|
|
116
|
-
};
|
|
56
|
+
export type SessionFeed = DashboardSessionFeed;
|
|
117
57
|
|
|
118
58
|
export type Identity = { user: { email?: string; hostedDomain?: string } };
|
|
119
59
|
|
|
@@ -141,7 +81,11 @@ export type SessionFilter = "active" | "recent" | "hung" | "failed" | "all";
|
|
|
141
81
|
|
|
142
82
|
export type VisualStatus = "active" | "failed" | "hung" | "idle";
|
|
143
83
|
|
|
144
|
-
export type CodeBlock = {
|
|
84
|
+
export type CodeBlock = {
|
|
85
|
+
code: string;
|
|
86
|
+
fenced?: boolean;
|
|
87
|
+
language: BundledLanguage;
|
|
88
|
+
};
|
|
145
89
|
|
|
146
90
|
export type MarkupNode =
|
|
147
91
|
| {
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AgentPluginRoute,
|
|
3
|
+
defineJuniorPlugin,
|
|
4
|
+
type JuniorPlugin,
|
|
5
|
+
} from "@sentry/junior-plugin-api";
|
|
6
|
+
import { buildDashboardConversationURL, normalizeDashboardPath } from "./url";
|
|
7
|
+
import { createDashboardApp, type JuniorDashboardOptions } from "./app";
|
|
8
|
+
|
|
9
|
+
export { createDashboardApp, type JuniorDashboardOptions } from "./app";
|
|
10
|
+
|
|
11
|
+
export interface JuniorDashboardPluginOptions extends JuniorDashboardOptions {
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function dashboardRoutePaths(options: JuniorDashboardPluginOptions): string[] {
|
|
16
|
+
const basePath = normalizeDashboardPath(options.basePath, "/");
|
|
17
|
+
const authPath = normalizeDashboardPath(options.authPath, "/api/auth");
|
|
18
|
+
const pagePaths =
|
|
19
|
+
basePath === "/"
|
|
20
|
+
? ["/", "/conversations", "/conversations/*", "/sessions", "/sessions/*"]
|
|
21
|
+
: [basePath, `${basePath}/*`];
|
|
22
|
+
|
|
23
|
+
return [
|
|
24
|
+
...pagePaths,
|
|
25
|
+
"/favicon.ico",
|
|
26
|
+
"/api/dashboard/*",
|
|
27
|
+
authPath,
|
|
28
|
+
`${authPath}/*`,
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function dashboardRoutes(
|
|
33
|
+
options: JuniorDashboardPluginOptions,
|
|
34
|
+
): AgentPluginRoute[] {
|
|
35
|
+
let app: ReturnType<typeof createDashboardApp> | undefined;
|
|
36
|
+
const fetch = (request: Request) => {
|
|
37
|
+
app ??= createDashboardApp(options);
|
|
38
|
+
return app.fetch(request);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return dashboardRoutePaths(options).map((path) => ({
|
|
42
|
+
handler: fetch,
|
|
43
|
+
path,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Register dashboard routes and Slack footer links through trusted plugin hooks. */
|
|
48
|
+
export function juniorDashboardPlugin(
|
|
49
|
+
options: JuniorDashboardPluginOptions = {},
|
|
50
|
+
): JuniorPlugin {
|
|
51
|
+
return defineJuniorPlugin({
|
|
52
|
+
name: "dashboard",
|
|
53
|
+
hooks: {
|
|
54
|
+
routes() {
|
|
55
|
+
if (options.disabled) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
return dashboardRoutes(options);
|
|
59
|
+
},
|
|
60
|
+
slackConversationLink(ctx) {
|
|
61
|
+
if (options.disabled) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
url: buildDashboardConversationURL({
|
|
66
|
+
basePath: options.basePath,
|
|
67
|
+
baseURL: options.baseURL,
|
|
68
|
+
conversationId: ctx.conversationId,
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
package/src/nitro.ts
CHANGED
|
@@ -104,7 +104,12 @@ function dashboardPageRoutes(
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Mount the authenticated Junior dashboard into a Nitro deployment.
|
|
109
|
+
*
|
|
110
|
+
* @deprecated Register `juniorDashboardPlugin()` in `createApp({ plugins })`;
|
|
111
|
+
* this helper remains for existing Nitro apps.
|
|
112
|
+
*/
|
|
108
113
|
export function juniorDashboardNitro(options: JuniorDashboardNitroOptions): {
|
|
109
114
|
nitro: { setup(nitro: unknown): void };
|
|
110
115
|
} {
|
package/src/url.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface DashboardBaseURLConfig {
|
|
2
|
+
baseURL?: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface DashboardConversationURLConfig extends DashboardBaseURLConfig {
|
|
6
|
+
basePath?: string;
|
|
7
|
+
conversationId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function withHttps(host: string): string {
|
|
11
|
+
return /^https?:\/\//.test(host) ? host : `https://${host}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function stripTrailingSlashes(value: string): string {
|
|
15
|
+
let end = value.length;
|
|
16
|
+
while (end > 1 && value.charCodeAt(end - 1) === 47) {
|
|
17
|
+
end -= 1;
|
|
18
|
+
}
|
|
19
|
+
return end === value.length ? value : value.slice(0, end);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Normalize dashboard route prefixes so plugin and Nitro paths agree. */
|
|
23
|
+
export function normalizeDashboardPath(
|
|
24
|
+
path: string | undefined,
|
|
25
|
+
fallback: string,
|
|
26
|
+
): string {
|
|
27
|
+
const value = path?.trim() || fallback;
|
|
28
|
+
const withSlash = value.startsWith("/") ? value : `/${value}`;
|
|
29
|
+
return stripTrailingSlashes(withSlash);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Resolve the dashboard origin used for browser auth and external links. */
|
|
33
|
+
export function resolveDashboardBaseURL(
|
|
34
|
+
config: DashboardBaseURLConfig = {},
|
|
35
|
+
): string {
|
|
36
|
+
const explicit =
|
|
37
|
+
config.baseURL ??
|
|
38
|
+
process.env.BETTER_AUTH_URL ??
|
|
39
|
+
process.env.JUNIOR_BASE_URL;
|
|
40
|
+
if (explicit?.trim()) {
|
|
41
|
+
return stripTrailingSlashes(withHttps(explicit.trim()));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const vercelProd = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim();
|
|
45
|
+
if (vercelProd) {
|
|
46
|
+
return stripTrailingSlashes(withHttps(vercelProd));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const vercelUrl = process.env.VERCEL_URL?.trim();
|
|
50
|
+
if (vercelUrl) {
|
|
51
|
+
return stripTrailingSlashes(withHttps(vercelUrl));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return "http://localhost:3000";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Build the dashboard conversation URL shown from outside the browser app. */
|
|
58
|
+
export function buildDashboardConversationURL(
|
|
59
|
+
config: DashboardConversationURLConfig,
|
|
60
|
+
): string {
|
|
61
|
+
const baseURL = resolveDashboardBaseURL({ baseURL: config.baseURL });
|
|
62
|
+
const basePath = normalizeDashboardPath(config.basePath, "/");
|
|
63
|
+
const path =
|
|
64
|
+
basePath === "/"
|
|
65
|
+
? `/conversations/${encodeURIComponent(config.conversationId)}`
|
|
66
|
+
: `${basePath}/conversations/${encodeURIComponent(config.conversationId)}`;
|
|
67
|
+
return `${baseURL}${path}`;
|
|
68
|
+
}
|