mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-11 01:45:58 +00:00
258 lines
7.7 KiB
TypeScript
258 lines
7.7 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
AlertCircleIcon,
|
|
CheckCircle2Icon,
|
|
LoaderCircleIcon,
|
|
PlugIcon,
|
|
UnplugIcon,
|
|
} from "lucide-react";
|
|
import { toast } from "sonner";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Item,
|
|
ItemActions,
|
|
ItemContent,
|
|
ItemDescription,
|
|
ItemMedia,
|
|
ItemTitle,
|
|
} from "@/components/ui/item";
|
|
import {
|
|
useChannelConnections,
|
|
useChannelProviders,
|
|
useConnectChannelProvider,
|
|
useDisconnectChannelConnection,
|
|
} from "@/core/channels/hooks";
|
|
import {
|
|
closeConnectWindow,
|
|
openConnectUrl,
|
|
prepareConnectWindow,
|
|
} from "@/core/channels/open-connect-url";
|
|
import type { ChannelConnection, ChannelProvider } from "@/core/channels/types";
|
|
import { useI18n } from "@/core/i18n/hooks";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
import { ChannelProviderIcon } from "../channels/channel-provider-icon";
|
|
|
|
import { SettingsSection } from "./settings-section";
|
|
|
|
function getProviderDescription(
|
|
provider: ChannelProvider,
|
|
descriptions: Record<string, string>,
|
|
): string {
|
|
return descriptions[provider.provider] ?? provider.display_name;
|
|
}
|
|
|
|
function getConnectionLabel(connection: ChannelConnection): string | null {
|
|
const account = connection.external_account_name;
|
|
const workspace = connection.workspace_name;
|
|
if (account && workspace) {
|
|
return `${account} · ${workspace}`;
|
|
}
|
|
return account ?? workspace ?? connection.external_account_id ?? null;
|
|
}
|
|
|
|
function getStatusLabel(
|
|
provider: ChannelProvider,
|
|
connection: ChannelConnection | undefined,
|
|
t: ReturnType<typeof useI18n>["t"],
|
|
): string {
|
|
if (!provider.enabled) {
|
|
return t.channels.disabled;
|
|
}
|
|
if (!provider.configured) {
|
|
return t.channels.unconfigured;
|
|
}
|
|
if (provider.unavailable_reason) {
|
|
return t.channels.unavailableShort;
|
|
}
|
|
const status = connection?.status ?? provider.connection_status;
|
|
if (status === "connected") {
|
|
return t.channels.connected;
|
|
}
|
|
if (status === "pending") {
|
|
return t.channels.pending;
|
|
}
|
|
if (status === "revoked") {
|
|
return t.channels.revoked;
|
|
}
|
|
return t.channels.notConnected;
|
|
}
|
|
|
|
function getProviderDisabledReason(
|
|
provider: ChannelProvider,
|
|
t: ReturnType<typeof useI18n>["t"],
|
|
): string | undefined {
|
|
if (!provider.enabled) {
|
|
return t.channels.disabled;
|
|
}
|
|
if (!provider.configured) {
|
|
return t.channels.unconfigured;
|
|
}
|
|
return provider.unavailable_reason ?? undefined;
|
|
}
|
|
|
|
function ChannelProviderItem({
|
|
provider,
|
|
connection,
|
|
}: {
|
|
provider: ChannelProvider;
|
|
connection?: ChannelConnection;
|
|
}) {
|
|
const { t } = useI18n();
|
|
const connectMutation = useConnectChannelProvider();
|
|
const disconnectMutation = useDisconnectChannelConnection();
|
|
const isConnected = connection?.status === "connected";
|
|
const canConnect =
|
|
(provider.connectable ?? (provider.enabled && provider.configured)) &&
|
|
!isConnected;
|
|
const isConnecting =
|
|
connectMutation.isPending &&
|
|
connectMutation.variables === provider.provider;
|
|
const isDisconnecting =
|
|
disconnectMutation.isPending &&
|
|
disconnectMutation.variables === connection?.id;
|
|
const connectionLabel = connection ? getConnectionLabel(connection) : null;
|
|
const statusLabel = getStatusLabel(provider, connection, t);
|
|
const unavailableReason = getProviderDisabledReason(provider, t);
|
|
|
|
return (
|
|
<Item variant="outline" className="w-full items-start">
|
|
<ItemMedia variant="icon" className="bg-background">
|
|
<ChannelProviderIcon provider={provider.provider} className="size-5" />
|
|
</ItemMedia>
|
|
<ItemContent className="min-w-0">
|
|
<ItemTitle className="w-full">
|
|
<span className="truncate">{provider.display_name}</span>
|
|
<Badge
|
|
variant={isConnected ? "default" : "outline"}
|
|
className={cn(!isConnected && "text-muted-foreground")}
|
|
>
|
|
{isConnected ? <CheckCircle2Icon /> : <AlertCircleIcon />}
|
|
{statusLabel}
|
|
</Badge>
|
|
</ItemTitle>
|
|
<ItemDescription className="line-clamp-none">
|
|
{getProviderDescription(provider, t.channels.descriptions)}
|
|
{connectionLabel ? ` ${t.channels.connectedAs(connectionLabel)}` : ""}
|
|
{!isConnected && provider.unavailable_reason
|
|
? ` ${provider.unavailable_reason}`
|
|
: ""}
|
|
</ItemDescription>
|
|
</ItemContent>
|
|
<ItemActions className="ml-auto">
|
|
{isConnected && connection ? (
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={isDisconnecting}
|
|
onClick={() => disconnectMutation.mutate(connection.id)}
|
|
>
|
|
{isDisconnecting ? (
|
|
<LoaderCircleIcon className="animate-spin" />
|
|
) : (
|
|
<UnplugIcon />
|
|
)}
|
|
{t.channels.disconnect}
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
disabled={!canConnect || isConnecting}
|
|
title={unavailableReason}
|
|
onClick={() => {
|
|
const connectWindow =
|
|
provider.auth_mode === "deep_link"
|
|
? prepareConnectWindow()
|
|
: null;
|
|
void connectMutation
|
|
.mutateAsync(provider.provider)
|
|
.then((result) => {
|
|
if (result.url) {
|
|
openConnectUrl(result.url, connectWindow);
|
|
return;
|
|
}
|
|
closeConnectWindow(connectWindow);
|
|
toast.success(result.instruction);
|
|
})
|
|
.catch((error) => {
|
|
closeConnectWindow(connectWindow);
|
|
toast.error(
|
|
error instanceof Error
|
|
? error.message
|
|
: t.channels.unavailable,
|
|
);
|
|
});
|
|
}}
|
|
>
|
|
{isConnecting ? (
|
|
<LoaderCircleIcon className="animate-spin" />
|
|
) : (
|
|
<PlugIcon />
|
|
)}
|
|
{connection?.status === "revoked"
|
|
? t.channels.reconnect
|
|
: t.channels.connect}
|
|
</Button>
|
|
)}
|
|
</ItemActions>
|
|
</Item>
|
|
);
|
|
}
|
|
|
|
export function ChannelsSettingsPage() {
|
|
const { t } = useI18n();
|
|
const {
|
|
enabled,
|
|
providers,
|
|
isLoading: providersLoading,
|
|
error: providersError,
|
|
} = useChannelProviders();
|
|
const {
|
|
connections,
|
|
isLoading: connectionsLoading,
|
|
error: connectionsError,
|
|
} = useChannelConnections();
|
|
const isLoading = providersLoading || connectionsLoading;
|
|
const error = providersError ?? connectionsError;
|
|
|
|
const connectionByProvider = new Map<string, ChannelConnection>();
|
|
for (const connection of connections) {
|
|
const existing = connectionByProvider.get(connection.provider);
|
|
if (!existing || connection.status === "connected") {
|
|
connectionByProvider.set(connection.provider, connection);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<SettingsSection
|
|
title={t.settings.channels.title}
|
|
description={t.settings.channels.description}
|
|
>
|
|
{isLoading ? (
|
|
<div className="text-muted-foreground text-sm">{t.common.loading}</div>
|
|
) : error ? (
|
|
<div className="text-destructive text-sm">{t.channels.unavailable}</div>
|
|
) : !enabled ? (
|
|
<div className="text-muted-foreground text-sm">
|
|
{t.settings.channels.disabled}
|
|
</div>
|
|
) : (
|
|
<div className="flex w-full flex-col gap-4">
|
|
{providers.map((provider) => (
|
|
<ChannelProviderItem
|
|
key={provider.provider}
|
|
provider={provider}
|
|
connection={connectionByProvider.get(provider.provider)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</SettingsSection>
|
|
);
|
|
}
|