mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
refactor(site): restructure agents routing and directory layout (#23408)
This commit is contained in:
@@ -0,0 +1,50 @@
|
|||||||
|
import { chatCostSummary } from "api/queries/chats";
|
||||||
|
import { useAuthContext } from "contexts/auth/AuthProvider";
|
||||||
|
import dayjs, { type Dayjs } from "dayjs";
|
||||||
|
import type { FC } from "react";
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
import { AgentAnalyticsPageView } from "./AgentAnalyticsPageView";
|
||||||
|
import { AgentPageHeader } from "./components/AgentPageHeader";
|
||||||
|
|
||||||
|
const createDateRange = (now?: Dayjs) => {
|
||||||
|
const end = now ?? dayjs();
|
||||||
|
const start = end.subtract(30, "day");
|
||||||
|
return {
|
||||||
|
startDate: start.toISOString(),
|
||||||
|
endDate: end.toISOString(),
|
||||||
|
rangeLabel: `${start.format("MMM D")} – ${end.format("MMM D, YYYY")}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AgentAnalyticsPageProps {
|
||||||
|
/** Override the current time for deterministic storybook snapshots. */
|
||||||
|
now?: Dayjs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AgentAnalyticsPage: FC<AgentAnalyticsPageProps> = ({ now }) => {
|
||||||
|
const { user } = useAuthContext();
|
||||||
|
const dateRange = createDateRange(now);
|
||||||
|
|
||||||
|
const summaryQuery = useQuery({
|
||||||
|
...chatCostSummary(user?.id ?? "me", {
|
||||||
|
start_date: dateRange.startDate,
|
||||||
|
end_date: dateRange.endDate,
|
||||||
|
}),
|
||||||
|
enabled: Boolean(user?.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AgentPageHeader />
|
||||||
|
<AgentAnalyticsPageView
|
||||||
|
summary={summaryQuery.data}
|
||||||
|
isLoading={summaryQuery.isLoading}
|
||||||
|
error={summaryQuery.error}
|
||||||
|
onRetry={() => void summaryQuery.refetch()}
|
||||||
|
rangeLabel={dateRange.rangeLabel}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AgentAnalyticsPage;
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { ChatCostSummary } from "api/typesGenerated";
|
||||||
|
import { BarChart3Icon } from "lucide-react";
|
||||||
|
import type { FC } from "react";
|
||||||
|
import { ChatCostSummaryView } from "./components/ChatCostSummaryView";
|
||||||
|
import { SectionHeader } from "./components/SectionHeader";
|
||||||
|
|
||||||
|
interface AgentAnalyticsPageViewProps {
|
||||||
|
summary: ChatCostSummary | undefined;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: unknown;
|
||||||
|
onRetry: () => void;
|
||||||
|
rangeLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AgentAnalyticsPageView: FC<AgentAnalyticsPageViewProps> = ({
|
||||||
|
summary,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
onRetry,
|
||||||
|
rangeLabel,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto p-4 pt-8 [scrollbar-width:thin] [scrollbar-color:hsl(var(--surface-quaternary))_transparent]">
|
||||||
|
<div className="mx-auto w-full max-w-3xl">
|
||||||
|
<SectionHeader
|
||||||
|
label="Analytics"
|
||||||
|
description="Review your personal chat usage and cost breakdowns."
|
||||||
|
action={
|
||||||
|
<div className="flex items-center gap-2 text-xs text-content-secondary">
|
||||||
|
<BarChart3Icon className="h-4 w-4" />
|
||||||
|
<span>{rangeLabel}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ChatCostSummaryView
|
||||||
|
summary={summary}
|
||||||
|
isLoading={isLoading}
|
||||||
|
error={error}
|
||||||
|
onRetry={onRetry}
|
||||||
|
loadingLabel="Loading analytics"
|
||||||
|
emptyMessage="No usage data for you in this period."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { chatModelConfigs, chatModels, createChat } from "api/queries/chats";
|
||||||
|
import type * as TypesGen from "api/typesGenerated";
|
||||||
|
import type { FC } from "react";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import {
|
||||||
|
AgentCreateForm,
|
||||||
|
type CreateChatOptions,
|
||||||
|
} from "./components/AgentCreateForm";
|
||||||
|
import { AgentPageHeader } from "./components/AgentPageHeader";
|
||||||
|
import { ChimeButton } from "./components/ChimeButton";
|
||||||
|
import { WebPushButton } from "./components/WebPushButton";
|
||||||
|
import {
|
||||||
|
buildModelConfigIDByModelID,
|
||||||
|
getModelOptionsFromCatalog,
|
||||||
|
} from "./utils/modelOptions";
|
||||||
|
|
||||||
|
const lastModelConfigIDStorageKey = "agents.last-model-config-id";
|
||||||
|
const nilUUID = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
|
const AgentCreatePage: FC = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const chatModelsQuery = useQuery(chatModels());
|
||||||
|
const chatModelConfigsQuery = useQuery(chatModelConfigs());
|
||||||
|
const createMutation = useMutation(createChat(queryClient));
|
||||||
|
|
||||||
|
const catalogModelOptions = getModelOptionsFromCatalog(
|
||||||
|
chatModelsQuery.data,
|
||||||
|
chatModelConfigsQuery.data,
|
||||||
|
);
|
||||||
|
const modelConfigIDByModelID = buildModelConfigIDByModelID(
|
||||||
|
chatModelConfigsQuery.data,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCreateChat = async ({
|
||||||
|
message,
|
||||||
|
fileIDs,
|
||||||
|
workspaceId,
|
||||||
|
model,
|
||||||
|
}: CreateChatOptions) => {
|
||||||
|
const modelConfigID =
|
||||||
|
(model && modelConfigIDByModelID.get(model)) || nilUUID;
|
||||||
|
const content: TypesGen.ChatInputPart[] = [];
|
||||||
|
if (message.trim()) {
|
||||||
|
content.push({ type: "text", text: message });
|
||||||
|
}
|
||||||
|
if (fileIDs) {
|
||||||
|
for (const fileID of fileIDs) {
|
||||||
|
content.push({ type: "file", file_id: fileID });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const createdChat = await createMutation.mutateAsync({
|
||||||
|
content,
|
||||||
|
workspace_id: workspaceId,
|
||||||
|
model_config_id: modelConfigID,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
if (modelConfigID !== nilUUID) {
|
||||||
|
localStorage.setItem(lastModelConfigIDStorageKey, modelConfigID);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(lastModelConfigIDStorageKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(`/agents/${createdChat.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenAnalytics = () => navigate("/agents/analytics");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AgentPageHeader>
|
||||||
|
<ChimeButton />
|
||||||
|
<WebPushButton />
|
||||||
|
</AgentPageHeader>
|
||||||
|
<AgentCreateForm
|
||||||
|
onCreateChat={handleCreateChat}
|
||||||
|
isCreating={createMutation.isPending}
|
||||||
|
createError={createMutation.error}
|
||||||
|
modelCatalog={chatModelsQuery.data}
|
||||||
|
modelOptions={catalogModelOptions}
|
||||||
|
modelConfigs={chatModelConfigsQuery.data ?? []}
|
||||||
|
isModelCatalogLoading={chatModelsQuery.isLoading}
|
||||||
|
isModelConfigsLoading={chatModelConfigsQuery.isLoading}
|
||||||
|
modelCatalogError={chatModelsQuery.error}
|
||||||
|
onOpenAnalytics={handleOpenAnalytics}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AgentCreatePage;
|
||||||
@@ -51,49 +51,7 @@ const AgentDetailLayout: FC = () => {
|
|||||||
requestUnarchiveAgent: () => {},
|
requestUnarchiveAgent: () => {},
|
||||||
isSidebarCollapsed: false,
|
isSidebarCollapsed: false,
|
||||||
onToggleSidebarCollapsed: () => {},
|
onToggleSidebarCollapsed: () => {},
|
||||||
modelOptions: [
|
onExpandSidebar: () => {},
|
||||||
{
|
|
||||||
id: "openai:gpt-4o",
|
|
||||||
provider: "openai",
|
|
||||||
model: "gpt-4o",
|
|
||||||
displayName: "GPT-4o",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
modelConfigIDByModelID: new Map([["openai:gpt-4o", "config-1"]]),
|
|
||||||
modelIDByConfigID: new Map([["config-1", "openai:gpt-4o"]]),
|
|
||||||
modelConfigs: [
|
|
||||||
{
|
|
||||||
id: "config-1",
|
|
||||||
provider: "openai",
|
|
||||||
model: "gpt-4o",
|
|
||||||
display_name: "GPT-4o",
|
|
||||||
enabled: true,
|
|
||||||
is_default: false,
|
|
||||||
context_limit: 200000,
|
|
||||||
compression_threshold: 70,
|
|
||||||
created_at: "2026-01-01T00:00:00Z",
|
|
||||||
updated_at: "2026-01-01T00:00:00Z",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
modelCatalog: {
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provider: "openai",
|
|
||||||
available: true,
|
|
||||||
models: [
|
|
||||||
{
|
|
||||||
id: "openai:gpt-4o",
|
|
||||||
provider: "openai",
|
|
||||||
model: "gpt-4o",
|
|
||||||
display_name: "GPT-4o",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
isModelCatalogLoading: false,
|
|
||||||
modelCatalogError: null,
|
|
||||||
desktopEnabled: false,
|
|
||||||
} satisfies AgentsOutletContext
|
} satisfies AgentsOutletContext
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { act, renderHook } from "@testing-library/react";
|
import { act, renderHook } from "@testing-library/react";
|
||||||
import { createRef } from "react";
|
import { createRef } from "react";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import type { ChatMessageInputRef } from "./AgentChatInput";
|
|
||||||
import {
|
import {
|
||||||
draftInputStorageKeyPrefix,
|
draftInputStorageKeyPrefix,
|
||||||
useConversationEditingState,
|
useConversationEditingState,
|
||||||
} from "./AgentDetail";
|
} from "./AgentDetail";
|
||||||
|
import type { ChatMessageInputRef } from "./components/AgentChatInput";
|
||||||
|
|
||||||
describe("useConversationEditingState", () => {
|
describe("useConversationEditingState", () => {
|
||||||
const chatID = "chat-abc-123";
|
const chatID = "chat-abc-123";
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import { API, watchWorkspace } from "api/api";
|
|||||||
import { isApiError } from "api/errors";
|
import { isApiError } from "api/errors";
|
||||||
import {
|
import {
|
||||||
chat,
|
chat,
|
||||||
|
chatDesktopEnabled,
|
||||||
chatMessagesForInfiniteScroll,
|
chatMessagesForInfiniteScroll,
|
||||||
|
chatModelConfigs,
|
||||||
|
chatModels,
|
||||||
createChatMessage,
|
createChatMessage,
|
||||||
deleteChatQueuedMessage,
|
deleteChatQueuedMessage,
|
||||||
editChatMessage,
|
editChatMessage,
|
||||||
@@ -33,24 +36,33 @@ import type { UrlTransform } from "streamdown";
|
|||||||
import { isMobileViewport } from "utils/mobile";
|
import { isMobileViewport } from "utils/mobile";
|
||||||
import { pageTitle } from "utils/page";
|
import { pageTitle } from "utils/page";
|
||||||
import { portForwardURL } from "utils/portForward";
|
import { portForwardURL } from "utils/portForward";
|
||||||
import type { ChatMessageInputRef } from "./AgentChatInput";
|
import type { AgentsOutletContext } from "./AgentsPage";
|
||||||
import { useChatStore } from "./AgentDetail/ChatContext";
|
import type { ChatMessageInputRef } from "./components/AgentChatInput";
|
||||||
import { getParentChatID, getWorkspaceAgent } from "./AgentDetail/chatHelpers";
|
import { useChatStore } from "./components/AgentDetail/ChatContext";
|
||||||
import { useWorkspaceCreationWatcher } from "./AgentDetail/useWorkspaceCreationWatcher";
|
import {
|
||||||
|
getParentChatID,
|
||||||
|
getWorkspaceAgent,
|
||||||
|
} from "./components/AgentDetail/chatHelpers";
|
||||||
|
import { useWorkspaceCreationWatcher } from "./components/AgentDetail/useWorkspaceCreationWatcher";
|
||||||
import {
|
import {
|
||||||
AgentDetailLoadingView,
|
AgentDetailLoadingView,
|
||||||
AgentDetailNotFoundView,
|
AgentDetailNotFoundView,
|
||||||
AgentDetailView,
|
AgentDetailView,
|
||||||
} from "./AgentDetailView";
|
} from "./components/AgentDetailView";
|
||||||
import type { AgentsOutletContext } from "./AgentsPage";
|
import { useGitWatcher } from "./hooks/useGitWatcher";
|
||||||
import {
|
import {
|
||||||
|
buildModelConfigIDByModelID,
|
||||||
|
buildModelIDByConfigID,
|
||||||
getModelCatalogStatusMessage,
|
getModelCatalogStatusMessage,
|
||||||
|
getModelOptionsFromCatalog,
|
||||||
getModelSelectorPlaceholder,
|
getModelSelectorPlaceholder,
|
||||||
hasConfiguredModelsInCatalog,
|
hasConfiguredModelsInCatalog,
|
||||||
} from "./modelOptions";
|
} from "./utils/modelOptions";
|
||||||
import { parsePullRequestUrl } from "./pullRequest";
|
import { parsePullRequestUrl } from "./utils/pullRequest";
|
||||||
import { formatUsageLimitMessage, isUsageLimitData } from "./usageLimitMessage";
|
import {
|
||||||
import { useGitWatcher } from "./useGitWatcher";
|
formatUsageLimitMessage,
|
||||||
|
isUsageLimitData,
|
||||||
|
} from "./utils/usageLimitMessage";
|
||||||
|
|
||||||
/** localStorage key controlling whether the right panel is visible. */
|
/** localStorage key controlling whether the right panel is visible. */
|
||||||
export const RIGHT_PANEL_OPEN_KEY = "agents.right-panel-open";
|
export const RIGHT_PANEL_OPEN_KEY = "agents.right-panel-open";
|
||||||
@@ -222,12 +234,6 @@ export function useConversationEditingState(deps: {
|
|||||||
const AgentDetail: FC = () => {
|
const AgentDetail: FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { agentId } = useParams<{ agentId: string }>();
|
const { agentId } = useParams<{ agentId: string }>();
|
||||||
const outletContext = useOutletContext<AgentsOutletContext>();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const [selectedModel, setSelectedModel] = useState("");
|
|
||||||
const [pendingEditMessageId, setPendingEditMessageId] = useState<
|
|
||||||
number | null
|
|
||||||
>(null);
|
|
||||||
const {
|
const {
|
||||||
chatErrorReasons,
|
chatErrorReasons,
|
||||||
setChatErrorReason,
|
setChatErrorReason,
|
||||||
@@ -235,18 +241,14 @@ const AgentDetail: FC = () => {
|
|||||||
requestArchiveAgent,
|
requestArchiveAgent,
|
||||||
requestArchiveAndDeleteWorkspace,
|
requestArchiveAndDeleteWorkspace,
|
||||||
requestUnarchiveAgent,
|
requestUnarchiveAgent,
|
||||||
onOpenAnalytics,
|
|
||||||
isSidebarCollapsed,
|
isSidebarCollapsed,
|
||||||
onToggleSidebarCollapsed,
|
onToggleSidebarCollapsed,
|
||||||
modelOptions,
|
} = useOutletContext<AgentsOutletContext>();
|
||||||
modelConfigIDByModelID,
|
const queryClient = useQueryClient();
|
||||||
modelIDByConfigID,
|
const [selectedModel, setSelectedModel] = useState("");
|
||||||
modelConfigs,
|
const [pendingEditMessageId, setPendingEditMessageId] = useState<
|
||||||
modelCatalog,
|
number | null
|
||||||
isModelCatalogLoading,
|
>(null);
|
||||||
modelCatalogError,
|
|
||||||
desktopEnabled,
|
|
||||||
} = outletContext;
|
|
||||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const chatInputRef = useRef<ChatMessageInputRef | null>(null);
|
const chatInputRef = useRef<ChatMessageInputRef | null>(null);
|
||||||
const inputValueRef = useRef(
|
const inputValueRef = useRef(
|
||||||
@@ -293,6 +295,26 @@ const AgentDetail: FC = () => {
|
|||||||
enabled: Boolean(workspaceId),
|
enabled: Boolean(workspaceId),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const chatModelsQuery = useQuery(chatModels());
|
||||||
|
const chatModelConfigsQuery = useQuery(chatModelConfigs());
|
||||||
|
const desktopEnabledQuery = useQuery(chatDesktopEnabled());
|
||||||
|
const desktopEnabled = desktopEnabledQuery.data?.enable_desktop ?? false;
|
||||||
|
|
||||||
|
const modelOptions = getModelOptionsFromCatalog(
|
||||||
|
chatModelsQuery.data,
|
||||||
|
chatModelConfigsQuery.data,
|
||||||
|
);
|
||||||
|
const modelConfigIDByModelID = buildModelConfigIDByModelID(
|
||||||
|
chatModelConfigsQuery.data,
|
||||||
|
);
|
||||||
|
const modelIDByConfigID = buildModelIDByConfigID(modelConfigIDByModelID);
|
||||||
|
const modelConfigs = chatModelConfigsQuery.data ?? [];
|
||||||
|
const modelCatalog = chatModelsQuery.data;
|
||||||
|
const isModelCatalogLoading = chatModelsQuery.isLoading;
|
||||||
|
const modelCatalogError = chatModelsQuery.error;
|
||||||
|
|
||||||
|
const handleOpenAnalytics = () => navigate("/agents/analytics");
|
||||||
|
|
||||||
// Subscribe to live workspace updates so that agent status changes
|
// Subscribe to live workspace updates so that agent status changes
|
||||||
// (e.g. connected/disconnected) are reflected without a page refresh.
|
// (e.g. connected/disconnected) are reflected without a page refresh.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -839,7 +861,7 @@ const AgentDetail: FC = () => {
|
|||||||
isInterruptPending={interruptMutation.isPending}
|
isInterruptPending={interruptMutation.isPending}
|
||||||
isSidebarCollapsed={isSidebarCollapsed}
|
isSidebarCollapsed={isSidebarCollapsed}
|
||||||
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
|
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
|
||||||
onOpenAnalytics={onOpenAnalytics}
|
onOpenAnalytics={handleOpenAnalytics}
|
||||||
showSidebarPanel={showSidebarPanel}
|
showSidebarPanel={showSidebarPanel}
|
||||||
onSetShowSidebarPanel={handleSetShowSidebarPanel}
|
onSetShowSidebarPanel={handleSetShowSidebarPanel}
|
||||||
prNumber={prNumber}
|
prNumber={prNumber}
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ import { type FC, useEffect, useRef, useState } from "react";
|
|||||||
import { useMutation, useQueryClient } from "react-query";
|
import { useMutation, useQueryClient } from "react-query";
|
||||||
import { Outlet, useParams } from "react-router";
|
import { Outlet, useParams } from "react-router";
|
||||||
import type { AgentsOutletContext } from "./AgentsPage";
|
import type { AgentsOutletContext } from "./AgentsPage";
|
||||||
import { bootstrapChatEmbedSession, EmbedProvider } from "./EmbedContext";
|
import {
|
||||||
import type { ChatDetailError } from "./usageLimitMessage";
|
bootstrapChatEmbedSession,
|
||||||
|
EmbedProvider,
|
||||||
|
} from "./components/EmbedContext";
|
||||||
|
import type { ChatDetailError } from "./utils/usageLimitMessage";
|
||||||
|
|
||||||
type BootstrapMessage = {
|
type BootstrapMessage = {
|
||||||
type: "coder:vscode-auth-bootstrap";
|
type: "coder:vscode-auth-bootstrap";
|
||||||
@@ -121,16 +124,8 @@ const AgentEmbedPage: FC = () => {
|
|||||||
requestArchiveAndDeleteWorkspace,
|
requestArchiveAndDeleteWorkspace,
|
||||||
isSidebarCollapsed,
|
isSidebarCollapsed,
|
||||||
onToggleSidebarCollapsed,
|
onToggleSidebarCollapsed,
|
||||||
modelOptions: [],
|
onExpandSidebar: () => {},
|
||||||
modelConfigIDByModelID: new Map(),
|
|
||||||
modelIDByConfigID: new Map(),
|
|
||||||
modelConfigs: [],
|
|
||||||
modelCatalog: undefined,
|
|
||||||
isModelCatalogLoading: false,
|
|
||||||
modelCatalogError: null,
|
|
||||||
desktopEnabled: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// When signed out and not already bootstrapping, listen for the
|
// When signed out and not already bootstrapping, listen for the
|
||||||
// postMessage from the parent frame carrying the session token.
|
// postMessage from the parent frame carrying the session token.
|
||||||
const isAwaitingBootstrapMessage =
|
const isAwaitingBootstrapMessage =
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { useAuthenticated } from "hooks";
|
||||||
|
import type { FC } from "react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import { AgentSettingsPageView } from "./AgentSettingsPageView";
|
||||||
|
import { AgentPageHeader } from "./components/AgentPageHeader";
|
||||||
|
|
||||||
|
const AgentSettingsPage: FC = () => {
|
||||||
|
const { section } = useParams();
|
||||||
|
const { permissions } = useAuthenticated();
|
||||||
|
const isAgentsAdmin = permissions.editDeploymentConfig;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AgentPageHeader />
|
||||||
|
<AgentSettingsPageView
|
||||||
|
activeSection={section ?? "behavior"}
|
||||||
|
canManageChatModelConfigs={isAgentsAdmin}
|
||||||
|
canSetSystemPrompt={isAgentsAdmin}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AgentSettingsPage;
|
||||||
+5
-5
@@ -6,7 +6,7 @@ import { userByNameKey } from "api/queries/users";
|
|||||||
import type * as TypesGen from "api/typesGenerated";
|
import type * as TypesGen from "api/typesGenerated";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { expect, spyOn, userEvent, waitFor, within } from "storybook/test";
|
import { expect, spyOn, userEvent, waitFor, within } from "storybook/test";
|
||||||
import { SettingsPageContent } from "./SettingsPageContent";
|
import { AgentSettingsPageView } from "./AgentSettingsPageView";
|
||||||
|
|
||||||
// ── Usage mock helpers ─────────────────────────────────────────
|
// ── Usage mock helpers ─────────────────────────────────────────
|
||||||
|
|
||||||
@@ -128,8 +128,8 @@ const fixedNow = dayjs("2026-03-12T00:00:00Z");
|
|||||||
// ── Meta ───────────────────────────────────────────────────────
|
// ── Meta ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: "pages/AgentsPage/SettingsPageContent",
|
title: "pages/AgentsPage/AgentSettingsPageView",
|
||||||
component: SettingsPageContent,
|
component: AgentSettingsPageView,
|
||||||
decorators: [withAuthProvider, withDashboardProvider],
|
decorators: [withAuthProvider, withDashboardProvider],
|
||||||
args: {
|
args: {
|
||||||
activeSection: "behavior",
|
activeSection: "behavior",
|
||||||
@@ -161,10 +161,10 @@ const meta = {
|
|||||||
});
|
});
|
||||||
spyOn(API, "updateChatWorkspaceTTL").mockResolvedValue();
|
spyOn(API, "updateChatWorkspaceTTL").mockResolvedValue();
|
||||||
},
|
},
|
||||||
} satisfies Meta<typeof SettingsPageContent>;
|
} satisfies Meta<typeof AgentSettingsPageView>;
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof SettingsPageContent>;
|
type Story = StoryObj<typeof AgentSettingsPageView>;
|
||||||
|
|
||||||
// ── Behavior tab stories ───────────────────────────────────────
|
// ── Behavior tab stories ───────────────────────────────────────
|
||||||
|
|
||||||
+8
-8
@@ -56,12 +56,12 @@ import {
|
|||||||
DateRange,
|
DateRange,
|
||||||
type DateRangeValue,
|
type DateRangeValue,
|
||||||
} from "../TemplatePage/TemplateInsightsPage/DateRange";
|
} from "../TemplatePage/TemplateInsightsPage/DateRange";
|
||||||
import { ChatCostSummaryView } from "./ChatCostSummaryView";
|
import { ChatCostSummaryView } from "./components/ChatCostSummaryView";
|
||||||
import { ChatModelAdminPanel } from "./ChatModelAdminPanel/ChatModelAdminPanel";
|
import { ChatModelAdminPanel } from "./components/ChatModelAdminPanel/ChatModelAdminPanel";
|
||||||
import { InsightsContent } from "./InsightsContent";
|
import { InsightsContent } from "./components/InsightsContent";
|
||||||
import { LimitsTab } from "./LimitsTab";
|
import { LimitsTab } from "./components/LimitsTab";
|
||||||
import { MCPServerAdminPanel } from "./MCPServerAdminPanel";
|
import { MCPServerAdminPanel } from "./components/MCPServerAdminPanel";
|
||||||
import { SectionHeader } from "./SectionHeader";
|
import { SectionHeader } from "./components/SectionHeader";
|
||||||
|
|
||||||
const AdminBadge: FC = () => (
|
const AdminBadge: FC = () => (
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
@@ -487,7 +487,7 @@ const UsageContent: FC<UsageContentProps> = ({ now }) => {
|
|||||||
const textareaClassName =
|
const textareaClassName =
|
||||||
"max-h-[240px] w-full resize-none overflow-y-auto rounded-lg border border-border bg-surface-primary px-4 py-3 font-sans text-[13px] leading-relaxed text-content-primary placeholder:text-content-secondary focus:outline-none focus:ring-2 focus:ring-content-link/30 [scrollbar-width:thin]";
|
"max-h-[240px] w-full resize-none overflow-y-auto rounded-lg border border-border bg-surface-primary px-4 py-3 font-sans text-[13px] leading-relaxed text-content-primary placeholder:text-content-secondary focus:outline-none focus:ring-2 focus:ring-content-link/30 [scrollbar-width:thin]";
|
||||||
|
|
||||||
interface SettingsPageContentProps {
|
interface AgentSettingsPageViewProps {
|
||||||
activeSection: string;
|
activeSection: string;
|
||||||
canManageChatModelConfigs: boolean;
|
canManageChatModelConfigs: boolean;
|
||||||
canSetSystemPrompt: boolean;
|
canSetSystemPrompt: boolean;
|
||||||
@@ -496,7 +496,7 @@ interface SettingsPageContentProps {
|
|||||||
now?: dayjs.Dayjs;
|
now?: dayjs.Dayjs;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingsPageContent: FC<SettingsPageContentProps> = ({
|
export const AgentSettingsPageView: FC<AgentSettingsPageViewProps> = ({
|
||||||
activeSection,
|
activeSection,
|
||||||
canManageChatModelConfigs,
|
canManageChatModelConfigs,
|
||||||
canSetSystemPrompt,
|
canSetSystemPrompt,
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { act, renderHook } from "@testing-library/react";
|
import { act, renderHook } from "@testing-library/react";
|
||||||
import { beforeEach, describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
import { emptyInputStorageKey, useEmptyStateDraft } from "./AgentCreateForm";
|
import {
|
||||||
|
emptyInputStorageKey,
|
||||||
|
useEmptyStateDraft,
|
||||||
|
} from "./components/AgentCreateForm";
|
||||||
|
|
||||||
describe("useEmptyStateDraft", () => {
|
describe("useEmptyStateDraft", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ import { API, watchChats } from "api/api";
|
|||||||
import { getErrorMessage } from "api/errors";
|
import { getErrorMessage } from "api/errors";
|
||||||
import {
|
import {
|
||||||
archiveChat,
|
archiveChat,
|
||||||
chatDesktopEnabled,
|
|
||||||
chatDiffContentsKey,
|
chatDiffContentsKey,
|
||||||
chatKey,
|
chatKey,
|
||||||
chatModelConfigs,
|
chatModelConfigs,
|
||||||
chatModels,
|
chatModels,
|
||||||
createChat,
|
|
||||||
infiniteChats,
|
infiniteChats,
|
||||||
invalidateChatListQueries,
|
invalidateChatListQueries,
|
||||||
prependToInfiniteChatsCache,
|
prependToInfiniteChatsCache,
|
||||||
@@ -30,24 +28,14 @@ import {
|
|||||||
import { useNavigate, useParams } from "react-router";
|
import { useNavigate, useParams } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { createReconnectingWebSocket } from "utils/reconnectingWebSocket";
|
import { createReconnectingWebSocket } from "utils/reconnectingWebSocket";
|
||||||
import {
|
|
||||||
type CreateChatOptions,
|
|
||||||
emptyInputStorageKey,
|
|
||||||
} from "./AgentCreateForm";
|
|
||||||
import { maybePlayChime } from "./AgentDetail/useAgentChime";
|
|
||||||
import { AgentsPageView } from "./AgentsPageView";
|
import { AgentsPageView } from "./AgentsPageView";
|
||||||
import { resolveArchiveAndDeleteAction } from "./agentWorkspaceUtils";
|
import { emptyInputStorageKey } from "./components/AgentCreateForm";
|
||||||
import {
|
import { maybePlayChime } from "./components/AgentDetail/useAgentChime";
|
||||||
getModelOptionsFromCatalog,
|
import { useAgentsPageKeybindings } from "./hooks/useAgentsPageKeybindings";
|
||||||
getNormalizedModelRef,
|
import { useAgentsPWA } from "./hooks/useAgentsPWA";
|
||||||
} from "./modelOptions";
|
import { resolveArchiveAndDeleteAction } from "./utils/agentWorkspaceUtils";
|
||||||
import type { ChatDetailError } from "./usageLimitMessage";
|
import { getModelOptionsFromCatalog } from "./utils/modelOptions";
|
||||||
import { useAgentsPageKeybindings } from "./useAgentsPageKeybindings";
|
import type { ChatDetailError } from "./utils/usageLimitMessage";
|
||||||
import { useAgentsPWA } from "./useAgentsPWA";
|
|
||||||
|
|
||||||
const lastModelConfigIDStorageKey = "agents.last-model-config-id";
|
|
||||||
const nilUUID = "00000000-0000-0000-0000-000000000000";
|
|
||||||
const EMPTY_MODEL_CONFIGS: TypesGen.ChatModelConfig[] = [];
|
|
||||||
|
|
||||||
// Type guard for SSE events from the chat list watch endpoint.
|
// Type guard for SSE events from the chat list watch endpoint.
|
||||||
// Shallow-compare two ChatDiffStatus objects by their meaningful
|
// Shallow-compare two ChatDiffStatus objects by their meaningful
|
||||||
@@ -93,11 +81,9 @@ const AgentsPage: FC = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { agentId } = useParams();
|
const { agentId } = useParams();
|
||||||
const { permissions, user } = useAuthenticated();
|
const { permissions } = useAuthenticated();
|
||||||
const { appearance } = useDashboard();
|
const { appearance } = useDashboard();
|
||||||
const isAgentsAdmin =
|
const isAgentsAdmin = permissions.editDeploymentConfig;
|
||||||
permissions.editDeploymentConfig ||
|
|
||||||
user.roles.some((role) => role.name === "owner" || role.name === "admin");
|
|
||||||
|
|
||||||
const [archivedFilter, setArchivedFilter] = useState<"active" | "archived">(
|
const [archivedFilter, setArchivedFilter] = useState<"active" | "archived">(
|
||||||
"active",
|
"active",
|
||||||
@@ -149,10 +135,12 @@ const AgentsPage: FC = () => {
|
|||||||
const chatsQuery = useInfiniteQuery(
|
const chatsQuery = useInfiniteQuery(
|
||||||
infiniteChats({ archived: archivedFilter === "archived" }),
|
infiniteChats({ archived: archivedFilter === "archived" }),
|
||||||
);
|
);
|
||||||
|
// Model queries are kept here for the sidebar, which displays
|
||||||
|
// model info alongside each chat. Child routes that need models
|
||||||
|
// subscribe to the same queries independently — react-query
|
||||||
|
// deduplicates the requests.
|
||||||
const chatModelsQuery = useQuery(chatModels());
|
const chatModelsQuery = useQuery(chatModels());
|
||||||
const chatModelConfigsQuery = useQuery(chatModelConfigs());
|
const chatModelConfigsQuery = useQuery(chatModelConfigs());
|
||||||
const desktopEnabledQuery = useQuery(chatDesktopEnabled());
|
|
||||||
const createMutation = useMutation(createChat(queryClient));
|
|
||||||
const archiveChatBase = archiveChat(queryClient);
|
const archiveChatBase = archiveChat(queryClient);
|
||||||
const archiveAgentMutation = useMutation({
|
const archiveAgentMutation = useMutation({
|
||||||
...archiveChatBase,
|
...archiveChatBase,
|
||||||
@@ -208,24 +196,6 @@ const AgentsPage: FC = () => {
|
|||||||
chatModelsQuery.data,
|
chatModelsQuery.data,
|
||||||
chatModelConfigsQuery.data,
|
chatModelConfigsQuery.data,
|
||||||
);
|
);
|
||||||
const modelConfigIDByModelID = (() => {
|
|
||||||
const byModelID = new Map<string, string>();
|
|
||||||
for (const config of chatModelConfigsQuery.data ?? []) {
|
|
||||||
const { provider, model } = getNormalizedModelRef(config);
|
|
||||||
if (!provider || !model) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const colonRef = `${provider}:${model}`;
|
|
||||||
if (!byModelID.has(colonRef)) {
|
|
||||||
byModelID.set(colonRef, config.id);
|
|
||||||
}
|
|
||||||
const slashRef = `${provider}/${model}`;
|
|
||||||
if (!byModelID.has(slashRef)) {
|
|
||||||
byModelID.set(slashRef, config.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return byModelID;
|
|
||||||
})();
|
|
||||||
const setChatErrorReason = (chatId: string, reason: ChatDetailError) => {
|
const setChatErrorReason = (chatId: string, reason: ChatDetailError) => {
|
||||||
const trimmedMessage = reason.message.trim();
|
const trimmedMessage = reason.message.trim();
|
||||||
if (!chatId || !trimmedMessage) {
|
if (!chatId || !trimmedMessage) {
|
||||||
@@ -317,35 +287,6 @@ const AgentsPage: FC = () => {
|
|||||||
};
|
};
|
||||||
const handleToggleSidebarCollapsed = () =>
|
const handleToggleSidebarCollapsed = () =>
|
||||||
setIsSidebarCollapsed((prev) => !prev);
|
setIsSidebarCollapsed((prev) => !prev);
|
||||||
const handleCreateChat = async (options: CreateChatOptions) => {
|
|
||||||
const { message, fileIDs, workspaceId, model } = options;
|
|
||||||
const modelConfigID =
|
|
||||||
(model && modelConfigIDByModelID.get(model)) || nilUUID;
|
|
||||||
const content: TypesGen.ChatInputPart[] = [];
|
|
||||||
if (message.trim()) {
|
|
||||||
content.push({ type: "text", text: message });
|
|
||||||
}
|
|
||||||
if (fileIDs) {
|
|
||||||
for (const fileID of fileIDs) {
|
|
||||||
content.push({ type: "file", file_id: fileID });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const createdChat = await createMutation.mutateAsync({
|
|
||||||
content,
|
|
||||||
workspace_id: workspaceId,
|
|
||||||
model_config_id: modelConfigID,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
if (modelConfigID !== nilUUID) {
|
|
||||||
localStorage.setItem(lastModelConfigIDStorageKey, modelConfigID);
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem(lastModelConfigIDStorageKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate(`/agents/${createdChat.id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNewAgent = () => {
|
const handleNewAgent = () => {
|
||||||
// Only clear the draft when the user is already on the empty
|
// Only clear the draft when the user is already on the empty
|
||||||
@@ -559,10 +500,10 @@ const AgentsPage: FC = () => {
|
|||||||
agentId={agentId}
|
agentId={agentId}
|
||||||
chatList={chatList}
|
chatList={chatList}
|
||||||
catalogModelOptions={catalogModelOptions}
|
catalogModelOptions={catalogModelOptions}
|
||||||
modelConfigs={chatModelConfigsQuery.data ?? EMPTY_MODEL_CONFIGS}
|
modelConfigs={chatModelConfigsQuery.data ?? []}
|
||||||
logoUrl={appearance.logo_url}
|
logoUrl={appearance.logo_url}
|
||||||
handleNewAgent={handleNewAgent}
|
handleNewAgent={handleNewAgent}
|
||||||
isCreating={createMutation.isPending}
|
isCreating={false}
|
||||||
isArchiving={isArchiving}
|
isArchiving={isArchiving}
|
||||||
archivingChatId={archivingChatId}
|
archivingChatId={archivingChatId}
|
||||||
isChatsLoading={chatsQuery.isLoading}
|
isChatsLoading={chatsQuery.isLoading}
|
||||||
@@ -578,14 +519,6 @@ const AgentsPage: FC = () => {
|
|||||||
requestUnarchiveAgent={requestUnarchiveAgent}
|
requestUnarchiveAgent={requestUnarchiveAgent}
|
||||||
requestArchiveAndDeleteWorkspace={requestArchiveAndDeleteWorkspace}
|
requestArchiveAndDeleteWorkspace={requestArchiveAndDeleteWorkspace}
|
||||||
onToggleSidebarCollapsed={handleToggleSidebarCollapsed}
|
onToggleSidebarCollapsed={handleToggleSidebarCollapsed}
|
||||||
onCreateChat={handleCreateChat}
|
|
||||||
createError={createMutation.error}
|
|
||||||
modelCatalog={chatModelsQuery.data}
|
|
||||||
isModelCatalogLoading={chatModelsQuery.isLoading}
|
|
||||||
isModelConfigsLoading={chatModelConfigsQuery.isLoading}
|
|
||||||
modelCatalogError={chatModelsQuery.error}
|
|
||||||
modelConfigIDByModelID={modelConfigIDByModelID}
|
|
||||||
desktopEnabled={desktopEnabledQuery.data?.enable_desktop ?? false}
|
|
||||||
isAgentsAdmin={isAgentsAdmin}
|
isAgentsAdmin={isAgentsAdmin}
|
||||||
hasNextPage={chatsQuery.hasNextPage}
|
hasNextPage={chatsQuery.hasNextPage}
|
||||||
onLoadMore={() => void chatsQuery.fetchNextPage()}
|
onLoadMore={() => void chatsQuery.fetchNextPage()}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { MockUserOwner } from "testHelpers/entities";
|
import {
|
||||||
|
MockNoPermissions,
|
||||||
|
MockPermissions,
|
||||||
|
MockUserOwner,
|
||||||
|
} from "testHelpers/entities";
|
||||||
import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook";
|
import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook";
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import { API } from "api/api";
|
import { API } from "api/api";
|
||||||
@@ -18,6 +22,9 @@ import {
|
|||||||
within,
|
within,
|
||||||
} from "storybook/test";
|
} from "storybook/test";
|
||||||
import { reactRouterParameters } from "storybook-addon-remix-react-router";
|
import { reactRouterParameters } from "storybook-addon-remix-react-router";
|
||||||
|
import AgentAnalyticsPage from "./AgentAnalyticsPage";
|
||||||
|
import AgentCreatePage from "./AgentCreatePage";
|
||||||
|
import AgentSettingsPage from "./AgentSettingsPage";
|
||||||
import { AgentsPageView } from "./AgentsPageView";
|
import { AgentsPageView } from "./AgentsPageView";
|
||||||
|
|
||||||
const defaultModelOptions: ModelSelectorOption[] = [
|
const defaultModelOptions: ModelSelectorOption[] = [
|
||||||
@@ -103,9 +110,6 @@ const mockUsageUsers: TypesGen.ChatCostUsersResponse = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use local noon so the rendered range label stays stable across timezones.
|
|
||||||
const fixedAnalyticsNow = dayjs("2026-03-12T12:00:00");
|
|
||||||
|
|
||||||
const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
||||||
const todayTimestamp = new Date().toISOString();
|
const todayTimestamp = new Date().toISOString();
|
||||||
|
|
||||||
@@ -123,16 +127,21 @@ const buildChat = (overrides: Partial<Chat> = {}): Chat => ({
|
|||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
const agentsRouting = [
|
// Use local noon so the rendered range label stays stable
|
||||||
{ path: "/agents/settings/:section", useStoryElement: true },
|
// across timezones.
|
||||||
{ path: "/agents/settings", useStoryElement: true },
|
const fixedNow = dayjs("2026-03-12T12:00:00");
|
||||||
{ path: "/agents/analytics", useStoryElement: true },
|
|
||||||
{ path: "/agents/:agentId", useStoryElement: true },
|
const agentsRouting = {
|
||||||
{ path: "/agents", useStoryElement: true },
|
path: "/agents",
|
||||||
] satisfies [
|
useStoryElement: true,
|
||||||
{ path: string; useStoryElement: boolean },
|
children: [
|
||||||
...{ path: string; useStoryElement: boolean }[],
|
{ path: "settings", element: <AgentSettingsPage /> },
|
||||||
];
|
{ path: "settings/:section", element: <AgentSettingsPage /> },
|
||||||
|
{ path: "analytics", element: <AgentAnalyticsPage now={fixedNow} /> },
|
||||||
|
{ path: ":agentId", element: <div /> },
|
||||||
|
{ index: true, element: <AgentCreatePage /> },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const meta: Meta<typeof AgentsPageView> = {
|
const meta: Meta<typeof AgentsPageView> = {
|
||||||
title: "pages/AgentsPage/AgentsPageView",
|
title: "pages/AgentsPage/AgentsPageView",
|
||||||
@@ -141,6 +150,7 @@ const meta: Meta<typeof AgentsPageView> = {
|
|||||||
parameters: {
|
parameters: {
|
||||||
layout: "fullscreen",
|
layout: "fullscreen",
|
||||||
user: MockUserOwner,
|
user: MockUserOwner,
|
||||||
|
permissions: MockPermissions,
|
||||||
reactRouter: reactRouterParameters({
|
reactRouter: reactRouterParameters({
|
||||||
location: { path: "/agents" },
|
location: { path: "/agents" },
|
||||||
routing: agentsRouting,
|
routing: agentsRouting,
|
||||||
@@ -170,35 +180,11 @@ const meta: Meta<typeof AgentsPageView> = {
|
|||||||
requestArchiveAndDeleteWorkspace: fn(),
|
requestArchiveAndDeleteWorkspace: fn(),
|
||||||
onToggleSidebarCollapsed: fn(),
|
onToggleSidebarCollapsed: fn(),
|
||||||
isAgentsAdmin: false,
|
isAgentsAdmin: false,
|
||||||
analyticsNow: fixedAnalyticsNow,
|
|
||||||
archivedFilter: "active" as const,
|
archivedFilter: "active" as const,
|
||||||
onArchivedFilterChange: fn(),
|
onArchivedFilterChange: fn(),
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
onLoadMore: fn(),
|
onLoadMore: fn(),
|
||||||
isFetchingNextPage: false,
|
isFetchingNextPage: false,
|
||||||
onCreateChat: fn(),
|
|
||||||
createError: undefined,
|
|
||||||
modelCatalog: {
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provider: "openai",
|
|
||||||
available: true,
|
|
||||||
models: [
|
|
||||||
{
|
|
||||||
id: "openai:gpt-4o",
|
|
||||||
provider: "openai",
|
|
||||||
model: "gpt-4o",
|
|
||||||
display_name: "GPT-4o",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
isModelCatalogLoading: false,
|
|
||||||
isModelConfigsLoading: false,
|
|
||||||
modelCatalogError: undefined,
|
|
||||||
modelConfigIDByModelID: new Map(),
|
|
||||||
desktopEnabled: false,
|
|
||||||
},
|
},
|
||||||
beforeEach: () => {
|
beforeEach: () => {
|
||||||
spyOn(API, "getWorkspaces").mockResolvedValue({
|
spyOn(API, "getWorkspaces").mockResolvedValue({
|
||||||
@@ -217,6 +203,44 @@ const meta: Meta<typeof AgentsPageView> = {
|
|||||||
spyOn(API, "updateUserChatCustomPrompt").mockResolvedValue({
|
spyOn(API, "updateUserChatCustomPrompt").mockResolvedValue({
|
||||||
custom_prompt: "",
|
custom_prompt: "",
|
||||||
});
|
});
|
||||||
|
// Mocks for child route pages that fetch their own data.
|
||||||
|
spyOn(API, "getChatModels").mockResolvedValue({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provider: "openai",
|
||||||
|
available: true,
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: "openai:gpt-4o",
|
||||||
|
provider: "openai",
|
||||||
|
model: "gpt-4o",
|
||||||
|
display_name: "GPT-4o",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
spyOn(API, "getChatModelConfigs").mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "config-openai-gpt-4o",
|
||||||
|
provider: "openai",
|
||||||
|
model: "gpt-4o",
|
||||||
|
display_name: "GPT-4o",
|
||||||
|
enabled: true,
|
||||||
|
is_default: false,
|
||||||
|
context_limit: 200000,
|
||||||
|
compression_threshold: 70,
|
||||||
|
created_at: "2026-02-18T00:00:00.000Z",
|
||||||
|
updated_at: "2026-02-18T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
spyOn(API, "getChatDesktopEnabled").mockResolvedValue({
|
||||||
|
enable_desktop: false,
|
||||||
|
});
|
||||||
|
spyOn(API, "getChatWorkspaceTTL").mockResolvedValue({
|
||||||
|
workspace_ttl_ms: 0,
|
||||||
|
});
|
||||||
|
spyOn(API, "updateChatWorkspaceTTL").mockResolvedValue();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -489,6 +513,9 @@ export const OpensAnalyticsForNonAdmins: Story = {
|
|||||||
args: {
|
args: {
|
||||||
isAgentsAdmin: false,
|
isAgentsAdmin: false,
|
||||||
},
|
},
|
||||||
|
parameters: {
|
||||||
|
permissions: MockNoPermissions,
|
||||||
|
},
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
await openAnalyticsView(canvasElement);
|
await openAnalyticsView(canvasElement);
|
||||||
|
|
||||||
@@ -523,6 +550,9 @@ export const OpensSettingsForNonAdmins: Story = {
|
|||||||
args: {
|
args: {
|
||||||
isAgentsAdmin: false,
|
isAgentsAdmin: false,
|
||||||
},
|
},
|
||||||
|
parameters: {
|
||||||
|
permissions: MockNoPermissions,
|
||||||
|
},
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
await openSettingsView(canvasElement);
|
await openSettingsView(canvasElement);
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,14 @@
|
|||||||
import type * as TypesGen from "api/typesGenerated";
|
import type * as TypesGen from "api/typesGenerated";
|
||||||
import type { ModelSelectorOption } from "components/ai-elements";
|
import type { ModelSelectorOption } from "components/ai-elements";
|
||||||
import { Button } from "components/Button/Button";
|
|
||||||
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
|
||||||
import { CoderIcon } from "components/Icons/CoderIcon";
|
|
||||||
import type { Dayjs } from "dayjs";
|
|
||||||
import { PanelLeftIcon } from "lucide-react";
|
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { NavLink, Outlet, useLocation, useNavigate } from "react-router";
|
import { Outlet, useLocation } from "react-router";
|
||||||
import { cn } from "utils/cn";
|
import { cn } from "utils/cn";
|
||||||
import { pageTitle } from "utils/page";
|
import { pageTitle } from "utils/page";
|
||||||
import { AgentCreateForm, type CreateChatOptions } from "./AgentCreateForm";
|
import {
|
||||||
import { AgentsSidebar, sidebarViewFromPath } from "./AgentsSidebar";
|
AgentsSidebar,
|
||||||
import { AnalyticsPageContent } from "./AnalyticsPageContent";
|
sidebarViewFromPath,
|
||||||
import { ChimeButton } from "./ChimeButton";
|
} from "./components/Sidebar/AgentsSidebar";
|
||||||
import { SettingsPageContent } from "./SettingsPageContent";
|
import type { ChatDetailError } from "./utils/usageLimitMessage";
|
||||||
import type { ChatDetailError } from "./usageLimitMessage";
|
|
||||||
import { WebPushButton } from "./WebPushButton";
|
|
||||||
|
|
||||||
type ChatModelOption = ModelSelectorOption;
|
|
||||||
|
|
||||||
export interface AgentsOutletContext {
|
export interface AgentsOutletContext {
|
||||||
chatErrorReasons: Record<string, ChatDetailError>;
|
chatErrorReasons: Record<string, ChatDetailError>;
|
||||||
@@ -31,21 +22,13 @@ export interface AgentsOutletContext {
|
|||||||
) => void;
|
) => void;
|
||||||
isSidebarCollapsed: boolean;
|
isSidebarCollapsed: boolean;
|
||||||
onToggleSidebarCollapsed: () => void;
|
onToggleSidebarCollapsed: () => void;
|
||||||
onOpenAnalytics?: () => void;
|
onExpandSidebar: () => void;
|
||||||
modelOptions: readonly ModelSelectorOption[];
|
|
||||||
modelConfigIDByModelID: ReadonlyMap<string, string>;
|
|
||||||
modelIDByConfigID: ReadonlyMap<string, string>;
|
|
||||||
modelConfigs: readonly TypesGen.ChatModelConfig[];
|
|
||||||
modelCatalog: TypesGen.ChatModelsResponse | null | undefined;
|
|
||||||
isModelCatalogLoading: boolean;
|
|
||||||
modelCatalogError: unknown;
|
|
||||||
desktopEnabled: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AgentsPageViewProps {
|
interface AgentsPageViewProps {
|
||||||
agentId: string | undefined;
|
agentId: string | undefined;
|
||||||
chatList: TypesGen.Chat[];
|
chatList: TypesGen.Chat[];
|
||||||
catalogModelOptions: readonly ChatModelOption[];
|
catalogModelOptions: readonly ModelSelectorOption[];
|
||||||
modelConfigs: readonly TypesGen.ChatModelConfig[];
|
modelConfigs: readonly TypesGen.ChatModelConfig[];
|
||||||
logoUrl: string;
|
logoUrl: string;
|
||||||
handleNewAgent: () => void;
|
handleNewAgent: () => void;
|
||||||
@@ -69,20 +52,11 @@ interface AgentsPageViewProps {
|
|||||||
) => void;
|
) => void;
|
||||||
onToggleSidebarCollapsed: () => void;
|
onToggleSidebarCollapsed: () => void;
|
||||||
isAgentsAdmin: boolean;
|
isAgentsAdmin: boolean;
|
||||||
onCreateChat: (options: CreateChatOptions) => Promise<void>;
|
|
||||||
createError: unknown;
|
|
||||||
modelCatalog: TypesGen.ChatModelsResponse | null | undefined;
|
|
||||||
isModelCatalogLoading: boolean;
|
|
||||||
isModelConfigsLoading: boolean;
|
|
||||||
modelCatalogError: unknown;
|
|
||||||
modelConfigIDByModelID: ReadonlyMap<string, string>;
|
|
||||||
desktopEnabled: boolean;
|
|
||||||
hasNextPage: boolean | undefined;
|
hasNextPage: boolean | undefined;
|
||||||
onLoadMore: () => void;
|
onLoadMore: () => void;
|
||||||
isFetchingNextPage: boolean;
|
isFetchingNextPage: boolean;
|
||||||
archivedFilter: "active" | "archived";
|
archivedFilter: "active" | "archived";
|
||||||
onArchivedFilterChange: (filter: "active" | "archived") => void;
|
onArchivedFilterChange: (filter: "active" | "archived") => void;
|
||||||
analyticsNow?: Dayjs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AgentsPageView: FC<AgentsPageViewProps> = ({
|
export const AgentsPageView: FC<AgentsPageViewProps> = ({
|
||||||
@@ -109,29 +83,15 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
|
|||||||
requestArchiveAndDeleteWorkspace,
|
requestArchiveAndDeleteWorkspace,
|
||||||
onToggleSidebarCollapsed,
|
onToggleSidebarCollapsed,
|
||||||
isAgentsAdmin,
|
isAgentsAdmin,
|
||||||
onCreateChat,
|
|
||||||
createError,
|
|
||||||
modelCatalog,
|
|
||||||
isModelCatalogLoading,
|
|
||||||
isModelConfigsLoading,
|
|
||||||
modelCatalogError,
|
|
||||||
modelConfigIDByModelID,
|
|
||||||
desktopEnabled,
|
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
isFetchingNextPage,
|
isFetchingNextPage,
|
||||||
archivedFilter,
|
archivedFilter,
|
||||||
onArchivedFilterChange,
|
onArchivedFilterChange,
|
||||||
analyticsNow,
|
|
||||||
}) => {
|
}) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
|
||||||
const sidebarView = sidebarViewFromPath(location.pathname);
|
const sidebarView = sidebarViewFromPath(location.pathname);
|
||||||
|
|
||||||
const handleOpenAnalytics = () => {
|
|
||||||
navigate("/agents/analytics");
|
|
||||||
};
|
|
||||||
|
|
||||||
// The sidebar expects plain string error messages, but the outlet
|
// The sidebar expects plain string error messages, but the outlet
|
||||||
// context now carries structured ChatDetailError objects.
|
// context now carries structured ChatDetailError objects.
|
||||||
const sidebarChatErrorReasons = Object.fromEntries(
|
const sidebarChatErrorReasons = Object.fromEntries(
|
||||||
@@ -141,16 +101,6 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const modelIDByConfigID = (() => {
|
|
||||||
const byConfigID = new Map<string, string>();
|
|
||||||
for (const [modelID, configID] of modelConfigIDByModelID.entries()) {
|
|
||||||
if (!byConfigID.has(configID)) {
|
|
||||||
byConfigID.set(configID, modelID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return byConfigID;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const outletContextValue: AgentsOutletContext = {
|
const outletContextValue: AgentsOutletContext = {
|
||||||
chatErrorReasons,
|
chatErrorReasons,
|
||||||
setChatErrorReason,
|
setChatErrorReason,
|
||||||
@@ -160,15 +110,7 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
|
|||||||
requestArchiveAndDeleteWorkspace,
|
requestArchiveAndDeleteWorkspace,
|
||||||
isSidebarCollapsed,
|
isSidebarCollapsed,
|
||||||
onToggleSidebarCollapsed,
|
onToggleSidebarCollapsed,
|
||||||
onOpenAnalytics: handleOpenAnalytics,
|
onExpandSidebar,
|
||||||
modelOptions: catalogModelOptions,
|
|
||||||
modelConfigIDByModelID,
|
|
||||||
modelIDByConfigID,
|
|
||||||
modelConfigs,
|
|
||||||
modelCatalog,
|
|
||||||
isModelCatalogLoading,
|
|
||||||
modelCatalogError,
|
|
||||||
desktopEnabled,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -217,60 +159,7 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
|
|||||||
"order-1 md:order-none flex-none md:flex-1",
|
"order-1 md:order-none flex-none md:flex-1",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{sidebarView.panel === "settings" ? (
|
<Outlet context={outletContextValue} />
|
||||||
<SettingsPageContent
|
|
||||||
activeSection={sidebarView.section}
|
|
||||||
canManageChatModelConfigs={isAgentsAdmin}
|
|
||||||
canSetSystemPrompt={isAgentsAdmin}
|
|
||||||
/>
|
|
||||||
) : sidebarView.panel === "analytics" ? (
|
|
||||||
<AnalyticsPageContent now={analyticsNow} />
|
|
||||||
) : agentId ? (
|
|
||||||
<Outlet key={agentId} context={outletContextValue} />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="flex shrink-0 items-center gap-2 px-4 py-0.5">
|
|
||||||
<NavLink
|
|
||||||
to="/workspaces"
|
|
||||||
className="inline-flex shrink-0 md:hidden"
|
|
||||||
>
|
|
||||||
{logoUrl ? (
|
|
||||||
<ExternalImage className="h-6" src={logoUrl} alt="Logo" />
|
|
||||||
) : (
|
|
||||||
<CoderIcon className="h-6 w-6 fill-content-primary" />
|
|
||||||
)}
|
|
||||||
</NavLink>
|
|
||||||
{isSidebarCollapsed && (
|
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
size="icon"
|
|
||||||
onClick={onExpandSidebar}
|
|
||||||
aria-label="Expand sidebar"
|
|
||||||
className="hidden h-7 w-7 min-w-0 shrink-0 md:inline-flex"
|
|
||||||
>
|
|
||||||
<PanelLeftIcon />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<div className="flex min-w-0 flex-1 items-center" />
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ChimeButton />
|
|
||||||
<WebPushButton />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<AgentCreateForm
|
|
||||||
onCreateChat={onCreateChat}
|
|
||||||
isCreating={isCreating}
|
|
||||||
createError={createError}
|
|
||||||
modelCatalog={modelCatalog}
|
|
||||||
modelOptions={catalogModelOptions}
|
|
||||||
modelConfigs={modelConfigs}
|
|
||||||
isModelCatalogLoading={isModelCatalogLoading}
|
|
||||||
isModelConfigsLoading={isModelConfigsLoading}
|
|
||||||
modelCatalogError={modelCatalogError}
|
|
||||||
onOpenAnalytics={handleOpenAnalytics}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
import { chatCostSummary } from "api/queries/chats";
|
|
||||||
import { useAuthContext } from "contexts/auth/AuthProvider";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { BarChart3Icon } from "lucide-react";
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { useQuery } from "react-query";
|
|
||||||
import { ChatCostSummaryView } from "./ChatCostSummaryView";
|
|
||||||
import { SectionHeader } from "./SectionHeader";
|
|
||||||
|
|
||||||
const createDateRange = (now?: dayjs.Dayjs) => {
|
|
||||||
const end = now ?? dayjs();
|
|
||||||
const start = end.subtract(30, "day");
|
|
||||||
return {
|
|
||||||
startDate: start.toISOString(),
|
|
||||||
endDate: end.toISOString(),
|
|
||||||
rangeLabel: `${start.format("MMM D")} – ${end.format("MMM D, YYYY")}`,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
interface AnalyticsPageContentProps {
|
|
||||||
/** Override the current time for date range calculation.
|
|
||||||
* Used for deterministic Storybook snapshots.
|
|
||||||
*/
|
|
||||||
now?: dayjs.Dayjs;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AnalyticsPageContent: FC<AnalyticsPageContentProps> = ({
|
|
||||||
now,
|
|
||||||
}) => {
|
|
||||||
const { user } = useAuthContext();
|
|
||||||
const dateRange = createDateRange(now);
|
|
||||||
|
|
||||||
const summaryQuery = useQuery({
|
|
||||||
...chatCostSummary(user?.id ?? "me", {
|
|
||||||
start_date: dateRange.startDate,
|
|
||||||
end_date: dateRange.endDate,
|
|
||||||
}),
|
|
||||||
enabled: Boolean(user?.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto p-4 pt-8 [scrollbar-width:thin] [scrollbar-color:hsl(var(--surface-quaternary))_transparent]">
|
|
||||||
<div className="mx-auto w-full max-w-3xl">
|
|
||||||
<SectionHeader
|
|
||||||
label="Analytics"
|
|
||||||
description="Review your personal chat usage and cost breakdowns."
|
|
||||||
action={
|
|
||||||
<div className="flex items-center gap-2 text-xs text-content-secondary">
|
|
||||||
<BarChart3Icon className="h-4 w-4" />
|
|
||||||
<span>{dateRange.rangeLabel}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ChatCostSummaryView
|
|
||||||
summary={summaryQuery.data}
|
|
||||||
isLoading={summaryQuery.isLoading}
|
|
||||||
error={summaryQuery.error}
|
|
||||||
onRetry={() => void summaryQuery.refetch()}
|
|
||||||
loadingLabel="Loading analytics"
|
|
||||||
emptyMessage="No usage data for you in this period."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+1
-1
@@ -36,8 +36,8 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { cn } from "utils/cn";
|
import { cn } from "utils/cn";
|
||||||
import { isMobileViewport } from "utils/mobile";
|
import { isMobileViewport } from "utils/mobile";
|
||||||
|
import { formatProviderLabel } from "../utils/modelOptions";
|
||||||
import { ImageLightbox } from "./ImageLightbox";
|
import { ImageLightbox } from "./ImageLightbox";
|
||||||
import { formatProviderLabel } from "./modelOptions";
|
|
||||||
import { QueuedMessagesList } from "./QueuedMessagesList";
|
import { QueuedMessagesList } from "./QueuedMessagesList";
|
||||||
|
|
||||||
export type { ChatMessageInputRef } from "components/ChatMessageInput/ChatMessageInput";
|
export type { ChatMessageInputRef } from "components/ChatMessageInput/ChatMessageInput";
|
||||||
+7
-4
@@ -24,15 +24,18 @@ import { useDashboard } from "modules/dashboard/useDashboard";
|
|||||||
import { type FC, useEffect, useRef, useState } from "react";
|
import { type FC, useEffect, useRef, useState } from "react";
|
||||||
import { useQuery } from "react-query";
|
import { useQuery } from "react-query";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AgentChatInput } from "./AgentChatInput";
|
import { useFileAttachments } from "../hooks/useFileAttachments";
|
||||||
import {
|
import {
|
||||||
getModelCatalogStatusMessage,
|
getModelCatalogStatusMessage,
|
||||||
getModelSelectorPlaceholder,
|
getModelSelectorPlaceholder,
|
||||||
getNormalizedModelRef,
|
getNormalizedModelRef,
|
||||||
hasConfiguredModelsInCatalog,
|
hasConfiguredModelsInCatalog,
|
||||||
} from "./modelOptions";
|
} from "../utils/modelOptions";
|
||||||
import { formatUsageLimitMessage, isUsageLimitData } from "./usageLimitMessage";
|
import {
|
||||||
import { useFileAttachments } from "./useFileAttachments";
|
formatUsageLimitMessage,
|
||||||
|
isUsageLimitData,
|
||||||
|
} from "../utils/usageLimitMessage";
|
||||||
|
import { AgentChatInput } from "./AgentChatInput";
|
||||||
|
|
||||||
/** @internal Exported for testing. */
|
/** @internal Exported for testing. */
|
||||||
export const emptyInputStorageKey = "agents.empty-input";
|
export const emptyInputStorageKey = "agents.empty-input";
|
||||||
+1
-1
@@ -6,7 +6,7 @@ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|||||||
import { type InfiniteData, useQueryClient } from "react-query";
|
import { type InfiniteData, useQueryClient } from "react-query";
|
||||||
import type { OneWayMessageEvent } from "utils/OneWayWebSocket";
|
import type { OneWayMessageEvent } from "utils/OneWayWebSocket";
|
||||||
import { createReconnectingWebSocket } from "utils/reconnectingWebSocket";
|
import { createReconnectingWebSocket } from "utils/reconnectingWebSocket";
|
||||||
import type { ChatDetailError } from "../usageLimitMessage";
|
import type { ChatDetailError } from "../../utils/usageLimitMessage";
|
||||||
import { applyMessagePartToStreamState } from "./streamState";
|
import { applyMessagePartToStreamState } from "./streamState";
|
||||||
import type { StreamState } from "./types";
|
import type { StreamState } from "./types";
|
||||||
|
|
||||||
+1
-1
@@ -29,9 +29,9 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import type { UrlTransform } from "streamdown";
|
import type { UrlTransform } from "streamdown";
|
||||||
import { cn } from "utils/cn";
|
import { cn } from "utils/cn";
|
||||||
|
import type { ChatDetailError } from "../../utils/usageLimitMessage";
|
||||||
import { ImageThumbnail } from "../AgentChatInput";
|
import { ImageThumbnail } from "../AgentChatInput";
|
||||||
import { ImageLightbox } from "../ImageLightbox";
|
import { ImageLightbox } from "../ImageLightbox";
|
||||||
import type { ChatDetailError } from "../usageLimitMessage";
|
|
||||||
import { useSmoothStreamingText } from "./SmoothText";
|
import { useSmoothStreamingText } from "./SmoothText";
|
||||||
import type {
|
import type {
|
||||||
MergedTool,
|
MergedTool,
|
||||||
+2
-2
@@ -27,9 +27,9 @@ import type { FC } from "react";
|
|||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { cn } from "utils/cn";
|
import { cn } from "utils/cn";
|
||||||
|
import { parsePullRequestUrl } from "../../utils/pullRequest";
|
||||||
import { useEmbedContext } from "../EmbedContext";
|
import { useEmbedContext } from "../EmbedContext";
|
||||||
import { PrStateIcon } from "../GitPanel";
|
import { PrStateIcon } from "../GitPanel/GitPanel";
|
||||||
import { parsePullRequestUrl } from "../pullRequest";
|
|
||||||
|
|
||||||
interface SidebarPanelState {
|
interface SidebarPanelState {
|
||||||
showSidebarPanel: boolean;
|
showSidebarPanel: boolean;
|
||||||
+2
-2
@@ -4,6 +4,8 @@ import { useDashboard } from "modules/dashboard/useDashboard";
|
|||||||
import { type FC, useEffect } from "react";
|
import { type FC, useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { UrlTransform } from "streamdown";
|
import type { UrlTransform } from "streamdown";
|
||||||
|
import { useFileAttachments } from "../hooks/useFileAttachments";
|
||||||
|
import type { ChatDetailError } from "../utils/usageLimitMessage";
|
||||||
import {
|
import {
|
||||||
AgentChatInput,
|
AgentChatInput,
|
||||||
type ChatMessageInputRef,
|
type ChatMessageInputRef,
|
||||||
@@ -30,8 +32,6 @@ import {
|
|||||||
} from "./AgentDetail/messageParsing";
|
} from "./AgentDetail/messageParsing";
|
||||||
import { buildStreamTools } from "./AgentDetail/streamState";
|
import { buildStreamTools } from "./AgentDetail/streamState";
|
||||||
import type { ParsedMessageEntry } from "./AgentDetail/types";
|
import type { ParsedMessageEntry } from "./AgentDetail/types";
|
||||||
import type { ChatDetailError } from "./usageLimitMessage";
|
|
||||||
import { useFileAttachments } from "./useFileAttachments";
|
|
||||||
|
|
||||||
type ChatStoreHandle = ReturnType<typeof useChatStore>["store"];
|
type ChatStoreHandle = ReturnType<typeof useChatStore>["store"];
|
||||||
|
|
||||||
+4
-4
@@ -7,6 +7,7 @@ import { type FC, type RefObject, useEffect, useRef, useState } from "react";
|
|||||||
import type { UrlTransform } from "streamdown";
|
import type { UrlTransform } from "streamdown";
|
||||||
import { cn } from "utils/cn";
|
import { cn } from "utils/cn";
|
||||||
import { pageTitle } from "utils/page";
|
import { pageTitle } from "utils/page";
|
||||||
|
import type { ChatDetailError } from "../utils/usageLimitMessage";
|
||||||
import { AgentChatInput, type ChatMessageInputRef } from "./AgentChatInput";
|
import { AgentChatInput, type ChatMessageInputRef } from "./AgentChatInput";
|
||||||
import {
|
import {
|
||||||
selectChatStatus,
|
selectChatStatus,
|
||||||
@@ -19,10 +20,9 @@ import {
|
|||||||
ChatConversationSkeleton,
|
ChatConversationSkeleton,
|
||||||
RightPanelSkeleton,
|
RightPanelSkeleton,
|
||||||
} from "./AgentsSkeletons";
|
} from "./AgentsSkeletons";
|
||||||
import { GitPanel } from "./GitPanel";
|
import { GitPanel } from "./GitPanel/GitPanel";
|
||||||
import { RightPanel } from "./RightPanel";
|
import { RightPanel } from "./RightPanel/RightPanel";
|
||||||
import { SidebarTabView } from "./SidebarTabView";
|
import { SidebarTabView } from "./Sidebar/SidebarTabView";
|
||||||
import type { ChatDetailError } from "./usageLimitMessage";
|
|
||||||
|
|
||||||
type ChatStoreHandle = ReturnType<typeof useChatStore>["store"];
|
type ChatStoreHandle = ReturnType<typeof useChatStore>["store"];
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Button } from "components/Button/Button";
|
||||||
|
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
||||||
|
import { CoderIcon } from "components/Icons/CoderIcon";
|
||||||
|
import { PanelLeftIcon } from "lucide-react";
|
||||||
|
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||||
|
import type { FC, ReactNode } from "react";
|
||||||
|
import { NavLink, useOutletContext } from "react-router";
|
||||||
|
import type { AgentsOutletContext } from "../AgentsPageView";
|
||||||
|
|
||||||
|
interface AgentPageHeaderProps {
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AgentPageHeader: FC<AgentPageHeaderProps> = ({ children }) => {
|
||||||
|
const { isSidebarCollapsed, onExpandSidebar } =
|
||||||
|
useOutletContext<AgentsOutletContext>();
|
||||||
|
const { appearance } = useDashboard();
|
||||||
|
const logoUrl = appearance.logo_url;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex shrink-0 items-center gap-2 px-4 py-0.5">
|
||||||
|
<NavLink to="/workspaces" className="inline-flex shrink-0 md:hidden">
|
||||||
|
{logoUrl ? (
|
||||||
|
<ExternalImage className="h-6" src={logoUrl} alt="Logo" />
|
||||||
|
) : (
|
||||||
|
<CoderIcon className="h-6 w-6 fill-content-primary" />
|
||||||
|
)}
|
||||||
|
</NavLink>
|
||||||
|
{isSidebarCollapsed && (
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
size="icon"
|
||||||
|
onClick={onExpandSidebar}
|
||||||
|
aria-label="Expand sidebar"
|
||||||
|
className="hidden h-7 w-7 min-w-0 shrink-0 md:inline-flex"
|
||||||
|
>
|
||||||
|
<PanelLeftIcon />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<div className="min-w-0 flex-1" />
|
||||||
|
{children && <div className="flex items-center gap-2">{children}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
-1
@@ -16,7 +16,7 @@ import { Spinner } from "components/Spinner/Spinner";
|
|||||||
import { type FC, type ReactNode, useState } from "react";
|
import { type FC, type ReactNode, useState } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||||
import { cn } from "utils/cn";
|
import { cn } from "utils/cn";
|
||||||
import { formatProviderLabel } from "../modelOptions";
|
import { formatProviderLabel } from "../../utils/modelOptions";
|
||||||
import { normalizeProvider, readOptionalString } from "./helpers";
|
import { normalizeProvider, readOptionalString } from "./helpers";
|
||||||
import { ModelsSection } from "./ModelsSection";
|
import { ModelsSection } from "./ModelsSection";
|
||||||
import { ProvidersSection } from "./ProvidersSection";
|
import { ProvidersSection } from "./ProvidersSection";
|
||||||
+1
-1
@@ -10,7 +10,7 @@ import {
|
|||||||
} from "components/Tooltip/Tooltip";
|
} from "components/Tooltip/Tooltip";
|
||||||
import { ChevronLeftIcon, InfoIcon } from "lucide-react";
|
import { ChevronLeftIcon, InfoIcon } from "lucide-react";
|
||||||
import { type FC, type FormEvent, useId, useState } from "react";
|
import { type FC, type FormEvent, useId, useState } from "react";
|
||||||
import { formatProviderLabel } from "../modelOptions";
|
import { formatProviderLabel } from "../../utils/modelOptions";
|
||||||
import type { ProviderState } from "./ChatModelAdminPanel";
|
import type { ProviderState } from "./ChatModelAdminPanel";
|
||||||
import { readOptionalString } from "./helpers";
|
import { readOptionalString } from "./helpers";
|
||||||
import { ProviderIcon } from "./ProviderIcon";
|
import { ProviderIcon } from "./ProviderIcon";
|
||||||
+1
-1
@@ -2,7 +2,7 @@ import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
|||||||
import { ServerIcon } from "lucide-react";
|
import { ServerIcon } from "lucide-react";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { cn } from "utils/cn";
|
import { cn } from "utils/cn";
|
||||||
import { formatProviderLabel } from "../modelOptions";
|
import { formatProviderLabel } from "../../utils/modelOptions";
|
||||||
import { normalizeProvider } from "./helpers";
|
import { normalizeProvider } from "./helpers";
|
||||||
|
|
||||||
const providerIconMap: Record<string, string> = {
|
const providerIconMap: Record<string, string> = {
|
||||||
+4
-4
@@ -12,9 +12,6 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import type { ChatMessageInputRef } from "./AgentChatInput";
|
|
||||||
import type { DiffStyle } from "./DiffViewer";
|
|
||||||
import { DiffViewer } from "./DiffViewer";
|
|
||||||
import {
|
import {
|
||||||
annotationLineForBox,
|
annotationLineForBox,
|
||||||
annotationSideForBox,
|
annotationSideForBox,
|
||||||
@@ -22,7 +19,10 @@ import {
|
|||||||
commentBoxFromRange,
|
commentBoxFromRange,
|
||||||
contentRangeForBox,
|
contentRangeForBox,
|
||||||
selectedLinesForBox,
|
selectedLinesForBox,
|
||||||
} from "./diffCommentSelection";
|
} from "../../utils/diffCommentSelection";
|
||||||
|
import type { ChatMessageInputRef } from "../AgentChatInput";
|
||||||
|
import type { DiffStyle } from "../DiffViewer/DiffViewer";
|
||||||
|
import { DiffViewer } from "../DiffViewer/DiffViewer";
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// Diff content extraction
|
// Diff content extraction
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import { DiffStatBadge } from "./DiffStats";
|
import { DiffStatBadge } from "../DiffViewer/DiffStats";
|
||||||
|
|
||||||
const badgeMeta: Meta<typeof DiffStatBadge> = {
|
const badgeMeta: Meta<typeof DiffStatBadge> = {
|
||||||
title: "pages/AgentsPage/DiffStatBadge",
|
title: "pages/AgentsPage/DiffStatBadge",
|
||||||
+3
-3
@@ -2,9 +2,9 @@ import type { DiffLineAnnotation, SelectedLineRange } from "@pierre/diffs";
|
|||||||
import { parsePatchFiles } from "@pierre/diffs";
|
import { parsePatchFiles } from "@pierre/diffs";
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import { expect, fn, waitFor } from "storybook/test";
|
import { expect, fn, waitFor } from "storybook/test";
|
||||||
import type { DiffStyle } from "./DiffViewer";
|
import type { DiffStyle } from "../DiffViewer/DiffViewer";
|
||||||
import { DiffViewer } from "./DiffViewer";
|
import { DiffViewer } from "../DiffViewer/DiffViewer";
|
||||||
import { InlinePromptInput } from "./RemoteDiffPanel";
|
import { InlinePromptInput } from "../DiffViewer/RemoteDiffPanel";
|
||||||
|
|
||||||
// biome-ignore format: raw diff string must preserve exact whitespace
|
// biome-ignore format: raw diff string must preserve exact whitespace
|
||||||
const sampleDiff = [
|
const sampleDiff = [
|
||||||
+1
-1
@@ -24,7 +24,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { cn } from "utils/cn";
|
import { cn } from "utils/cn";
|
||||||
import { changeColor, changeLabel } from "./diffColors";
|
import { changeColor, changeLabel } from "../../utils/diffColors";
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// Public interface
|
// Public interface
|
||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
import { parsePatchFiles } from "@pierre/diffs";
|
import { parsePatchFiles } from "@pierre/diffs";
|
||||||
import type { WorkspaceAgentRepoChanges } from "api/typesGenerated";
|
import type { WorkspaceAgentRepoChanges } from "api/typesGenerated";
|
||||||
import type { FC, RefObject } from "react";
|
import type { FC, RefObject } from "react";
|
||||||
import type { ChatMessageInputRef } from "./AgentChatInput";
|
import type { ChatMessageInputRef } from "../AgentChatInput";
|
||||||
import { CommentableDiffViewer } from "./CommentableDiffViewer";
|
import { CommentableDiffViewer } from "../DiffViewer/CommentableDiffViewer";
|
||||||
import type { DiffStyle } from "./DiffViewer";
|
import type { DiffStyle } from "../DiffViewer/DiffViewer";
|
||||||
|
|
||||||
interface LocalDiffPanelProps {
|
interface LocalDiffPanelProps {
|
||||||
repo: WorkspaceAgentRepoChanges;
|
repo: WorkspaceAgentRepoChanges;
|
||||||
+6
-6
@@ -14,13 +14,13 @@ import {
|
|||||||
import { type FC, type RefObject, useEffect, useState } from "react";
|
import { type FC, type RefObject, useEffect, useState } from "react";
|
||||||
import { useQuery } from "react-query";
|
import { useQuery } from "react-query";
|
||||||
import { cn } from "utils/cn";
|
import { cn } from "utils/cn";
|
||||||
import type { ChatMessageInputRef } from "./AgentChatInput";
|
import { parsePullRequestUrl } from "../../utils/pullRequest";
|
||||||
import { CommentableDiffViewer } from "./CommentableDiffViewer";
|
import type { ChatMessageInputRef } from "../AgentChatInput";
|
||||||
import { DiffStatBadge } from "./DiffStats";
|
import { CommentableDiffViewer } from "../DiffViewer/CommentableDiffViewer";
|
||||||
import type { DiffStyle } from "./DiffViewer";
|
import { DiffStatBadge } from "../DiffViewer/DiffStats";
|
||||||
import { parsePullRequestUrl } from "./pullRequest";
|
import type { DiffStyle } from "../DiffViewer/DiffViewer";
|
||||||
|
|
||||||
export { InlinePromptInput } from "./CommentableDiffViewer";
|
export { InlinePromptInput } from "../DiffViewer/CommentableDiffViewer";
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// PR state badge
|
// PR state badge
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { parsePatchFiles } from "@pierre/diffs";
|
import { parsePatchFiles } from "@pierre/diffs";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { extractDiffContent } from "./CommentableDiffViewer";
|
import { extractDiffContent } from "../DiffViewer/CommentableDiffViewer";
|
||||||
|
|
||||||
function parse(diffStr: string) {
|
function parse(diffStr: string) {
|
||||||
return parsePatchFiles(diffStr).flatMap((p) => p.files);
|
return parsePatchFiles(diffStr).flatMap((p) => p.files);
|
||||||
+9
-5
@@ -19,11 +19,15 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { type FC, type RefObject, useEffect, useRef, useState } from "react";
|
import { type FC, type RefObject, useEffect, useRef, useState } from "react";
|
||||||
import { cn } from "utils/cn";
|
import { cn } from "utils/cn";
|
||||||
import type { ChatMessageInputRef } from "./AgentChatInput";
|
import type { ChatMessageInputRef } from "../AgentChatInput";
|
||||||
import { DiffStatBadge } from "./DiffStats";
|
import { DiffStatBadge } from "../DiffViewer/DiffStats";
|
||||||
import { type DiffStyle, loadDiffStyle, saveDiffStyle } from "./DiffViewer";
|
import {
|
||||||
import { LocalDiffPanel } from "./LocalDiffPanel";
|
type DiffStyle,
|
||||||
import { RemoteDiffPanel } from "./RemoteDiffPanel";
|
loadDiffStyle,
|
||||||
|
saveDiffStyle,
|
||||||
|
} from "../DiffViewer/DiffViewer";
|
||||||
|
import { LocalDiffPanel } from "../DiffViewer/LocalDiffPanel";
|
||||||
|
import { RemoteDiffPanel } from "../DiffViewer/RemoteDiffPanel";
|
||||||
|
|
||||||
type GitView = { type: "remote" } | { type: "local"; repoRoot: string };
|
type GitView = { type: "remote" } | { type: "local"; repoRoot: string };
|
||||||
|
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import { expect, fn, userEvent, within } from "storybook/test";
|
import { expect, fn, userEvent, within } from "storybook/test";
|
||||||
import { InlinePromptInput } from "./RemoteDiffPanel";
|
import { InlinePromptInput } from "../DiffViewer/RemoteDiffPanel";
|
||||||
|
|
||||||
const meta: Meta<typeof InlinePromptInput> = {
|
const meta: Meta<typeof InlinePromptInput> = {
|
||||||
title: "pages/AgentsPage/InlinePromptInput",
|
title: "pages/AgentsPage/InlinePromptInput",
|
||||||
+1
-1
@@ -21,7 +21,7 @@ import type { FC } from "react";
|
|||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||||
import { cn } from "utils/cn";
|
import { cn } from "utils/cn";
|
||||||
import { formatCostMicros } from "utils/currency";
|
import { formatCostMicros } from "utils/currency";
|
||||||
import { PrStateIcon } from "./GitPanel";
|
import { PrStateIcon } from "./GitPanel/GitPanel";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user