chore: ran linting

This commit is contained in:
Anish Sarkar 2026-03-21 13:20:13 +05:30
parent 772150eb66
commit de8841fb86
110 changed files with 2673 additions and 1918 deletions

View file

@ -214,11 +214,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
if (!searchSpaceId) return null;
return (
<Dialog
open={isOpen}
modal={false}
onOpenChange={handleOpenChange}
>
<Dialog open={isOpen} modal={false} onOpenChange={handleOpenChange}>
{showTrigger && (
<TooltipIconButton
data-joyride="connector-icon"
@ -354,11 +350,12 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
onBack={handleBackFromEdit}
onQuickIndex={(() => {
const cfg = connectorConfig || editingConnector.config;
const isDrive = editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" ||
const isDrive =
editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" ||
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR";
const hasDriveItems = isDrive
? ((cfg?.selected_folders as unknown[]) ?? []).length > 0 ||
((cfg?.selected_files as unknown[]) ?? []).length > 0
((cfg?.selected_files as unknown[]) ?? []).length > 0
: true;
if (!hasDriveItems) return undefined;
return () => {
@ -376,37 +373,37 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
onNameChange={setConnectorName}
/>
) : indexingConfig ? (
<IndexingConfigurationView
config={indexingConfig}
connector={
indexingConnector
? {
...indexingConnector,
config: indexingConnectorConfig || indexingConnector.config,
}
: undefined
}
startDate={startDate}
endDate={endDate}
periodicEnabled={periodicEnabled}
frequencyMinutes={frequencyMinutes}
enableSummary={enableSummary}
isStartingIndexing={isStartingIndexing}
isFromOAuth={isFromOAuth}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
onPeriodicEnabledChange={setPeriodicEnabled}
onFrequencyChange={setFrequencyMinutes}
onEnableSummaryChange={setEnableSummary}
onConfigChange={setIndexingConnectorConfig}
onStartIndexing={() => {
if (indexingConfig.connectorId) {
startIndexing(indexingConfig.connectorId);
<IndexingConfigurationView
config={indexingConfig}
connector={
indexingConnector
? {
...indexingConnector,
config: indexingConnectorConfig || indexingConnector.config,
}
: undefined
}
handleStartIndexing(() => refreshConnectors());
}}
onSkip={handleSkipIndexing}
/>
startDate={startDate}
endDate={endDate}
periodicEnabled={periodicEnabled}
frequencyMinutes={frequencyMinutes}
enableSummary={enableSummary}
isStartingIndexing={isStartingIndexing}
isFromOAuth={isFromOAuth}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
onPeriodicEnabledChange={setPeriodicEnabled}
onFrequencyChange={setFrequencyMinutes}
onEnableSummaryChange={setEnableSummary}
onConfigChange={setIndexingConnectorConfig}
onStartIndexing={() => {
if (indexingConfig.connectorId) {
startIndexing(indexingConfig.connectorId);
}
handleStartIndexing(() => refreshConnectors());
}}
onSkip={handleSkipIndexing}
/>
) : (
<Tabs
value={activeTab}

View file

@ -1,7 +1,6 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Info } from "lucide-react";
import type { FC } from "react";
import { useId, useRef, useState } from "react";
@ -26,6 +25,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
Select,
SelectContent,

View file

@ -82,10 +82,7 @@ function getFileIconFromName(fileName: string, className: string = "size-3.5 shr
return <File className={`${className} text-gray-500`} />;
}
export const ComposioDriveConfig: FC<ConnectorConfigProps> = ({
connector,
onConfigChange,
}) => {
export const ComposioDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfigChange }) => {
const isIndexable = connector.config?.is_indexable as boolean;
const existingFolders =
@ -236,47 +233,48 @@ export const ComposioDriveConfig: FC<ConnectorConfigProps> = ({
</div>
)}
{isAuthExpired && (
<p className="text-xs text-amber-600 dark:text-amber-500">
Your Google Drive authentication has expired. Please re-authenticate using the button below.
</p>
)}
{isAuthExpired && (
<p className="text-xs text-amber-600 dark:text-amber-500">
Your Google Drive authentication has expired. Please re-authenticate using the button
below.
</p>
)}
{isEditMode ? (
<div className="space-y-2">
<button
type="button"
onClick={() => setIsFolderTreeOpen(!isFolderTreeOpen)}
className="flex items-center gap-2 text-xs sm:text-sm text-muted-foreground hover:text-foreground transition-colors w-fit"
>
Change Selection
{isFolderTreeOpen ? (
<ChevronDown className="size-4" />
) : (
<ChevronRight className="size-4" />
{isEditMode ? (
<div className="space-y-2">
<button
type="button"
onClick={() => setIsFolderTreeOpen(!isFolderTreeOpen)}
className="flex items-center gap-2 text-xs sm:text-sm text-muted-foreground hover:text-foreground transition-colors w-fit"
>
Change Selection
{isFolderTreeOpen ? (
<ChevronDown className="size-4" />
) : (
<ChevronRight className="size-4" />
)}
</button>
{isFolderTreeOpen && (
<ComposioDriveFolderTree
connectorId={connector.id}
selectedFolders={selectedFolders}
onSelectFolders={handleSelectFolders}
selectedFiles={selectedFiles}
onSelectFiles={handleSelectFiles}
onAuthError={handleAuthError}
/>
)}
</button>
{isFolderTreeOpen && (
<ComposioDriveFolderTree
connectorId={connector.id}
selectedFolders={selectedFolders}
onSelectFolders={handleSelectFolders}
selectedFiles={selectedFiles}
onSelectFiles={handleSelectFiles}
onAuthError={handleAuthError}
/>
)}
</div>
) : (
<ComposioDriveFolderTree
connectorId={connector.id}
selectedFolders={selectedFolders}
onSelectFolders={handleSelectFolders}
selectedFiles={selectedFiles}
onSelectFiles={handleSelectFiles}
onAuthError={handleAuthError}
/>
)}
</div>
) : (
<ComposioDriveFolderTree
connectorId={connector.id}
selectedFolders={selectedFolders}
onSelectFolders={handleSelectFolders}
selectedFiles={selectedFiles}
onSelectFiles={handleSelectFiles}
onAuthError={handleAuthError}
/>
)}
</div>
{/* Indexing Options */}

View file

@ -1,12 +1,12 @@
"use client";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { KeyRound, Server } from "lucide-react";
import type { FC } from "react";
import { useEffect, useId, useRef, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import type { ConnectorConfigProps } from "../index";
export interface ElasticsearchConfigProps extends ConnectorConfigProps {

View file

@ -231,26 +231,25 @@ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfi
</div>
)}
<Button
type="button"
variant="outline"
onClick={openPicker}
disabled={pickerLoading || isAuthExpired}
className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 hover:bg-slate-400/10 dark:hover:bg-white/10 text-xs sm:text-sm h-8 sm:h-9"
>
{pickerLoading && <Spinner size="xs" className="mr-1.5" />}
{totalSelected > 0 ? "Change Selection" : "Select from Google Drive"}
</Button>
<Button
type="button"
variant="outline"
onClick={openPicker}
disabled={pickerLoading || isAuthExpired}
className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 hover:bg-slate-400/10 dark:hover:bg-white/10 text-xs sm:text-sm h-8 sm:h-9"
>
{pickerLoading && <Spinner size="xs" className="mr-1.5" />}
{totalSelected > 0 ? "Change Selection" : "Select from Google Drive"}
</Button>
{pickerError && !isAuthExpired && (
<p className="text-xs text-destructive">{pickerError}</p>
)}
{pickerError && !isAuthExpired && <p className="text-xs text-destructive">{pickerError}</p>}
{isAuthExpired && (
<p className="text-xs text-amber-600 dark:text-amber-500">
Your Google Drive authentication has expired. Please re-authenticate using the button below.
</p>
)}
{isAuthExpired && (
<p className="text-xs text-amber-600 dark:text-amber-500">
Your Google Drive authentication has expired. Please re-authenticate using the button
below.
</p>
)}
</div>
{/* Indexing Options */}

View file

@ -220,10 +220,8 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
</p>
</div>
</div>
{/* Quick Index Button - hidden when auth is expired */}
{connector.is_indexable &&
onQuickIndex &&
!isAuthExpired && (
{/* Quick Index Button - hidden when auth is expired */}
{connector.is_indexable && onQuickIndex && !isAuthExpired && (
<Button
variant="secondary"
size="sm"
@ -401,31 +399,31 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
Disconnect
</Button>
)}
{isAuthExpired && reauthEndpoint ? (
<Button
onClick={handleReauth}
disabled={reauthing || isDisconnecting}
className="text-xs sm:text-sm flex-1 sm:flex-initial h-12 sm:h-auto py-3 sm:py-2 bg-amber-600 hover:bg-amber-700 text-white"
>
<RefreshCw className={cn("size-3.5", reauthing && "animate-spin")} />
Re-authenticate
</Button>
) : (
<Button
onClick={onSave}
disabled={isSaving || isDisconnecting}
className="text-xs sm:text-sm flex-1 sm:flex-initial h-12 sm:h-auto py-3 sm:py-2"
>
{isSaving ? (
<>
<Spinner size="sm" className="mr-2" />
Saving
</>
) : (
"Save Changes"
)}
</Button>
)}
{isAuthExpired && reauthEndpoint ? (
<Button
onClick={handleReauth}
disabled={reauthing || isDisconnecting}
className="text-xs sm:text-sm flex-1 sm:flex-initial h-12 sm:h-auto py-3 sm:py-2 bg-amber-600 hover:bg-amber-700 text-white"
>
<RefreshCw className={cn("size-3.5", reauthing && "animate-spin")} />
Re-authenticate
</Button>
) : (
<Button
onClick={onSave}
disabled={isSaving || isDisconnecting}
className="text-xs sm:text-sm flex-1 sm:flex-initial h-12 sm:h-auto py-3 sm:py-2"
>
{isSaving ? (
<>
<Spinner size="sm" className="mr-2" />
Saving
</>
) : (
"Save Changes"
)}
</Button>
)}
</div>
</div>
);

View file

@ -261,35 +261,28 @@ export const useConnectorDialog = () => {
| (typeof COMPOSIO_CONNECTORS)[number]
| undefined;
if (result.connectorId) {
const connectorId = parseInt(result.connectorId, 10);
newConnector = fetchResult.data.find(
(c: SearchSourceConnector) => c.id === connectorId
);
if (newConnector) {
const connectorType = newConnector.connector_type;
oauthConnector =
OAUTH_CONNECTORS.find(
(c) => c.connectorType === connectorType
) ||
COMPOSIO_CONNECTORS.find(
(c) => c.connectorType === connectorType
);
if (result.connectorId) {
const connectorId = parseInt(result.connectorId, 10);
newConnector = fetchResult.data.find((c: SearchSourceConnector) => c.id === connectorId);
if (newConnector) {
const connectorType = newConnector.connector_type;
oauthConnector =
OAUTH_CONNECTORS.find((c) => c.connectorType === connectorType) ||
COMPOSIO_CONNECTORS.find((c) => c.connectorType === connectorType);
}
}
}
if (!newConnector && result.connector) {
oauthConnector =
OAUTH_CONNECTORS.find((c) => c.id === result.connector) ||
COMPOSIO_CONNECTORS.find((c) => c.id === result.connector);
if (oauthConnector) {
const oauthType = oauthConnector.connectorType;
newConnector = fetchResult.data.find(
(c: SearchSourceConnector) =>
c.connector_type === oauthType
);
if (!newConnector && result.connector) {
oauthConnector =
OAUTH_CONNECTORS.find((c) => c.id === result.connector) ||
COMPOSIO_CONNECTORS.find((c) => c.id === result.connector);
if (oauthConnector) {
const oauthType = oauthConnector.connectorType;
newConnector = fetchResult.data.find(
(c: SearchSourceConnector) => c.connector_type === oauthType
);
}
}
}
if (newConnector && oauthConnector) {
const connectorValidation = searchSourceConnector.safeParse(newConnector);
@ -599,17 +592,17 @@ export const useConnectorDialog = () => {
: `${connectorTitle} connected and syncing started!`;
toast.success(successMessage);
setIsOpen(false);
setIsOpen(false);
setIndexingConfig(null);
setIndexingConnector(null);
setIndexingConnectorConfig(null);
setIndexingConfig(null);
setIndexingConnector(null);
setIndexingConnectorConfig(null);
queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
});
queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
});
await refetchAllConnectors();
await refetchAllConnectors();
} else {
// Non-indexable connector
// For Circleback, transition to edit view to show webhook URL
@ -631,11 +624,11 @@ export const useConnectorDialog = () => {
setStartDate(undefined);
setEndDate(undefined);
toast.success(`${connectorTitle} connected successfully!`, {
description: "Configure the webhook URL in your Circleback settings.",
});
toast.success(`${connectorTitle} connected successfully!`, {
description: "Configure the webhook URL in your Circleback settings.",
});
await refetchAllConnectors();
await refetchAllConnectors();
} else {
// Other non-indexable connectors - just show success message and close
const successMessage =
@ -644,13 +637,13 @@ export const useConnectorDialog = () => {
: `${connectorTitle} connected successfully!`;
toast.success(successMessage);
await refetchAllConnectors();
await refetchAllConnectors();
setIsOpen(false);
setIsOpen(false);
setIndexingConfig(null);
setIndexingConnector(null);
setIndexingConnectorConfig(null);
setIndexingConfig(null);
setIndexingConnector(null);
setIndexingConnectorConfig(null);
}
}
}
@ -870,12 +863,12 @@ export const useConnectorDialog = () => {
);
}
toast.success(`${indexingConfig.connectorTitle} indexing started`);
toast.success(`${indexingConfig.connectorTitle} indexing started`);
setIsOpen(false);
setIsFromOAuth(false);
setIsOpen(false);
setIsFromOAuth(false);
refreshConnectors();
refreshConnectors();
queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
});
@ -927,21 +920,21 @@ export const useConnectorDialog = () => {
return;
}
// Track if we came from accounts list view so handleBackFromEdit can restore it
if (viewingAccountsType && viewingAccountsType.connectorType === connector.connector_type) {
setCameFromAccountsList(viewingAccountsType);
} else {
setCameFromAccountsList(null);
}
setViewingAccountsType(null);
// Track if we came from accounts list view so handleBackFromEdit can restore it
if (viewingAccountsType && viewingAccountsType.connectorType === connector.connector_type) {
setCameFromAccountsList(viewingAccountsType);
} else {
setCameFromAccountsList(null);
}
setViewingAccountsType(null);
// Track if we came from MCP list view so handleBackFromEdit can restore it
if (viewingMCPList && connector.connector_type === "MCP_CONNECTOR") {
setCameFromMCPList(true);
} else {
setCameFromMCPList(false);
}
setViewingMCPList(false);
// Track if we came from MCP list view so handleBackFromEdit can restore it
if (viewingMCPList && connector.connector_type === "MCP_CONNECTOR") {
setCameFromMCPList(true);
} else {
setCameFromMCPList(false);
}
setViewingMCPList(false);
// Track index with date range opened event
if (connector.is_indexable) {
@ -952,15 +945,15 @@ export const useConnectorDialog = () => {
);
}
setEditingConnector(connector);
setConnectorName(connector.name);
setPeriodicEnabled(!connector.is_indexable ? false : connector.periodic_indexing_enabled);
setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440");
setEnableSummary(connector.enable_summary ?? false);
setStartDate(undefined);
setEndDate(undefined);
},
[searchSpaceId, viewingAccountsType, viewingMCPList, handleViewMCPList, activeTab]
setEditingConnector(connector);
setConnectorName(connector.name);
setPeriodicEnabled(!connector.is_indexable ? false : connector.periodic_indexing_enabled);
setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440");
setEnableSummary(connector.enable_summary ?? false);
setStartDate(undefined);
setEndDate(undefined);
},
[searchSpaceId, viewingAccountsType, viewingMCPList, handleViewMCPList, activeTab]
);
// Handle saving connector changes
@ -1139,35 +1132,35 @@ export const useConnectorDialog = () => {
: indexingDescription,
});
setIsOpen(false);
setIsOpen(false);
refreshConnectors();
queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
});
} catch (error) {
console.error("Error saving connector:", error);
toast.error("Failed to save connector changes");
} finally {
setIsSaving(false);
}
},
[
editingConnector,
searchSpaceId,
isSaving,
startDate,
endDate,
indexConnector,
updateConnector,
periodicEnabled,
frequencyMinutes,
enableSummary,
getFrequencyLabel,
connectorConfig,
connectorName,
setIsOpen,
]
refreshConnectors();
queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
});
} catch (error) {
console.error("Error saving connector:", error);
toast.error("Failed to save connector changes");
} finally {
setIsSaving(false);
}
},
[
editingConnector,
searchSpaceId,
isSaving,
startDate,
endDate,
indexConnector,
updateConnector,
periodicEnabled,
frequencyMinutes,
enableSummary,
getFrequencyLabel,
connectorConfig,
connectorName,
setIsOpen,
]
);
// Handle disconnecting connector
@ -1194,19 +1187,19 @@ export const useConnectorDialog = () => {
: `${editingConnector.name} disconnected successfully`
);
if (editingConnector.connector_type === "MCP_CONNECTOR" && cameFromMCPList) {
setViewingMCPList(true);
setEditingConnector(null);
setConnectorName(null);
setConnectorConfig(null);
} else {
setEditingConnector(null);
setConnectorName(null);
setConnectorConfig(null);
setIsOpen(false);
}
if (editingConnector.connector_type === "MCP_CONNECTOR" && cameFromMCPList) {
setViewingMCPList(true);
setEditingConnector(null);
setConnectorName(null);
setConnectorConfig(null);
} else {
setEditingConnector(null);
setConnectorName(null);
setConnectorConfig(null);
setIsOpen(false);
}
refreshConnectors();
refreshConnectors();
queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
});
@ -1312,13 +1305,13 @@ export const useConnectorDialog = () => {
setEditingConnector(null);
setConnectorName(null);
setConnectorConfig(null);
setConnectingConnectorType(null);
setViewingAccountsType(null);
setViewingMCPList(false);
setCameFromAccountsList(null);
setCameFromMCPList(false);
setConnectCameFromMCPList(false);
setStartDate(undefined);
setConnectingConnectorType(null);
setViewingAccountsType(null);
setViewingMCPList(false);
setCameFromAccountsList(null);
setCameFromMCPList(false);
setConnectCameFromMCPList(false);
setStartDate(undefined);
setEndDate(undefined);
setPeriodicEnabled(false);
setFrequencyMinutes("1440");

View file

@ -203,8 +203,7 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{typeConnectors.map((connector) => {
const isIndexing = indexingConnectorIds.has(connector.id);
const isAuthExpired =
!!reauthEndpoint && connector.config?.auth_expired === true;
const isAuthExpired = !!reauthEndpoint && connector.config?.auth_expired === true;
return (
<div
@ -252,7 +251,9 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
onClick={() => handleReauth(connector.id)}
disabled={reauthingId === connector.id}
>
<RefreshCw className={cn("size-3.5", reauthingId === connector.id && "animate-spin")} />
<RefreshCw
className={cn("size-3.5", reauthingId === connector.id && "animate-spin")}
/>
Re-authenticate
</Button>
) : (

View file

@ -32,7 +32,9 @@ export const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?:
const inProgressStep = steps.find((s) => getEffectiveStatus(s) === "in_progress");
const allCompleted =
steps.length > 0 && !isThreadRunning && steps.every((s) => getEffectiveStatus(s) === "completed");
steps.length > 0 &&
!isThreadRunning &&
steps.every((s) => getEffectiveStatus(s) === "completed");
const isProcessing = isThreadRunning && !allCompleted;
// Auto-collapse when all tasks are completed
@ -127,7 +129,7 @@ export const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?:
effectiveStatus === "pending" && "text-muted-foreground/60"
)}
>
{step.title}
{step.title}
</div>
{/* Step items (sub-content) */}

View file

@ -90,7 +90,11 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
import { Switch } from "@/components/ui/switch";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import { CONNECTOR_ICON_TO_TYPES, CONNECTOR_TOOL_ICON_PATHS, getToolIcon } from "@/contracts/enums/toolIcons";
import {
CONNECTOR_ICON_TO_TYPES,
CONNECTOR_TOOL_ICON_PATHS,
getToolIcon,
} from "@/contracts/enums/toolIcons";
import type { Document } from "@/contracts/types/document.types";
import { useBatchCommentsPreload } from "@/hooks/use-comments";
import { useCommentsElectric } from "@/hooks/use-comments-electric";
@ -735,71 +739,75 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
</span>
</div>
<div className="overflow-y-auto pb-6" onScroll={handleToolsScroll}>
{groupedTools.filter((g) => !g.connectorIcon).map((group) => (
<div key={group.label}>
<div className="px-4 pt-3 pb-1 text-xs text-muted-foreground/80 font-medium select-none">
{group.label}
{groupedTools
.filter((g) => !g.connectorIcon)
.map((group) => (
<div key={group.label}>
<div className="px-4 pt-3 pb-1 text-xs text-muted-foreground/80 font-medium select-none">
{group.label}
</div>
{group.tools.map((tool) => {
const isDisabled = disabledTools.includes(tool.name);
const ToolIcon = getToolIcon(tool.name);
return (
<div
key={tool.name}
className="flex w-full items-center gap-3 px-4 py-2 hover:bg-muted-foreground/10 transition-colors"
>
<ToolIcon className="size-4 shrink-0 text-muted-foreground" />
<span className="flex-1 min-w-0 text-sm font-medium truncate">
{formatToolName(tool.name)}
</span>
<Switch
checked={!isDisabled}
onCheckedChange={() => toggleTool(tool.name)}
className="shrink-0"
/>
</div>
);
})}
</div>
{group.tools.map((tool) => {
const isDisabled = disabledTools.includes(tool.name);
const ToolIcon = getToolIcon(tool.name);
return (
<div
key={tool.name}
className="flex w-full items-center gap-3 px-4 py-2 hover:bg-muted-foreground/10 transition-colors"
>
<ToolIcon className="size-4 shrink-0 text-muted-foreground" />
<span className="flex-1 min-w-0 text-sm font-medium truncate">
{formatToolName(tool.name)}
</span>
<Switch
checked={!isDisabled}
onCheckedChange={() => toggleTool(tool.name)}
className="shrink-0"
/>
</div>
);
})}
</div>
))}
))}
{groupedTools.some((g) => g.connectorIcon) && (
<div>
<div className="px-4 pt-3 pb-1 text-xs text-muted-foreground/80 font-medium select-none">
Connector Actions
</div>
{groupedTools.filter((g) => g.connectorIcon).map((group) => {
const iconKey = group.connectorIcon ?? "";
const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey];
const toolNames = group.tools.map((t) => t.name);
const allDisabled = toolNames.every((n) => disabledTools.includes(n));
return (
<div
key={group.label}
className="flex w-full items-center gap-3 px-4 py-2 hover:bg-muted-foreground/10 transition-colors"
>
{iconInfo ? (
<Image
src={iconInfo.src}
alt={iconInfo.alt}
width={18}
height={18}
className="size-[18px] shrink-0 select-none pointer-events-none"
draggable={false}
{groupedTools
.filter((g) => g.connectorIcon)
.map((group) => {
const iconKey = group.connectorIcon ?? "";
const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey];
const toolNames = group.tools.map((t) => t.name);
const allDisabled = toolNames.every((n) => disabledTools.includes(n));
return (
<div
key={group.label}
className="flex w-full items-center gap-3 px-4 py-2 hover:bg-muted-foreground/10 transition-colors"
>
{iconInfo ? (
<Image
src={iconInfo.src}
alt={iconInfo.alt}
width={18}
height={18}
className="size-[18px] shrink-0 select-none pointer-events-none"
draggable={false}
/>
) : (
<Wrench className="size-4 shrink-0 text-muted-foreground" />
)}
<span className="flex-1 min-w-0 text-sm font-medium truncate">
{group.label}
</span>
<Switch
checked={!allDisabled}
onCheckedChange={() => toggleToolGroup(toolNames)}
className="shrink-0"
/>
) : (
<Wrench className="size-4 shrink-0 text-muted-foreground" />
)}
<span className="flex-1 min-w-0 text-sm font-medium truncate">
{group.label}
</span>
<Switch
checked={!allDisabled}
onCheckedChange={() => toggleToolGroup(toolNames)}
className="shrink-0"
/>
</div>
);
})}
</div>
);
})}
</div>
)}
{!filteredTools?.length && (
@ -857,82 +865,87 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
WebkitMaskImage: `linear-gradient(to bottom, ${toolsScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${toolsScrollPos === "bottom" ? "black" : "transparent"})`,
}}
>
{groupedTools.filter((g) => !g.connectorIcon).map((group) => (
<div key={group.label}>
<div className="px-2.5 sm:px-3 pt-2 pb-0.5 text-[10px] sm:text-xs text-muted-foreground/80 font-normal select-none">
{group.label}
{groupedTools
.filter((g) => !g.connectorIcon)
.map((group) => (
<div key={group.label}>
<div className="px-2.5 sm:px-3 pt-2 pb-0.5 text-[10px] sm:text-xs text-muted-foreground/80 font-normal select-none">
{group.label}
</div>
{group.tools.map((tool) => {
const isDisabled = disabledTools.includes(tool.name);
const ToolIcon = getToolIcon(tool.name);
const row = (
<div className="flex w-full items-center gap-2 sm:gap-3 px-2.5 sm:px-3 py-1 sm:py-1.5 hover:bg-muted-foreground/10 transition-colors">
<ToolIcon className="size-3.5 sm:size-4 shrink-0 text-muted-foreground" />
<span className="flex-1 min-w-0 text-xs sm:text-sm font-medium truncate">
{formatToolName(tool.name)}
</span>
<Switch
checked={!isDisabled}
onCheckedChange={() => toggleTool(tool.name)}
className="shrink-0 scale-[0.6] sm:scale-75"
/>
</div>
);
return (
<Tooltip key={tool.name}>
<TooltipTrigger asChild>{row}</TooltipTrigger>
<TooltipContent side="right" className="max-w-64 text-xs">
{tool.description}
</TooltipContent>
</Tooltip>
);
})}
</div>
{group.tools.map((tool) => {
const isDisabled = disabledTools.includes(tool.name);
const ToolIcon = getToolIcon(tool.name);
const row = (
<div className="flex w-full items-center gap-2 sm:gap-3 px-2.5 sm:px-3 py-1 sm:py-1.5 hover:bg-muted-foreground/10 transition-colors">
<ToolIcon className="size-3.5 sm:size-4 shrink-0 text-muted-foreground" />
<span className="flex-1 min-w-0 text-xs sm:text-sm font-medium truncate">
{formatToolName(tool.name)}
</span>
<Switch
checked={!isDisabled}
onCheckedChange={() => toggleTool(tool.name)}
className="shrink-0 scale-[0.6] sm:scale-75"
/>
</div>
);
return (
<Tooltip key={tool.name}>
<TooltipTrigger asChild>{row}</TooltipTrigger>
<TooltipContent side="right" className="max-w-64 text-xs">
{tool.description}
</TooltipContent>
</Tooltip>
);
})}
</div>
))}
))}
{groupedTools.some((g) => g.connectorIcon) && (
<div>
<div className="px-2.5 sm:px-3 pt-2 pb-0.5 text-[10px] sm:text-xs text-muted-foreground/80 font-normal select-none">
Connector Actions
</div>
{groupedTools.filter((g) => g.connectorIcon).map((group) => {
const iconKey = group.connectorIcon ?? "";
const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey];
const toolNames = group.tools.map((t) => t.name);
const allDisabled = toolNames.every((n) => disabledTools.includes(n));
const groupDef = TOOL_GROUPS.find((g) => g.label === group.label);
const row = (
<div className="flex w-full items-center gap-2 sm:gap-3 px-2.5 sm:px-3 py-1 sm:py-1.5 hover:bg-muted-foreground/10 transition-colors">
{iconInfo ? (
<Image
src={iconInfo.src}
alt={iconInfo.alt}
width={16}
height={16}
className="size-3.5 sm:size-4 shrink-0 select-none pointer-events-none"
draggable={false}
{groupedTools
.filter((g) => g.connectorIcon)
.map((group) => {
const iconKey = group.connectorIcon ?? "";
const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey];
const toolNames = group.tools.map((t) => t.name);
const allDisabled = toolNames.every((n) => disabledTools.includes(n));
const groupDef = TOOL_GROUPS.find((g) => g.label === group.label);
const row = (
<div className="flex w-full items-center gap-2 sm:gap-3 px-2.5 sm:px-3 py-1 sm:py-1.5 hover:bg-muted-foreground/10 transition-colors">
{iconInfo ? (
<Image
src={iconInfo.src}
alt={iconInfo.alt}
width={16}
height={16}
className="size-3.5 sm:size-4 shrink-0 select-none pointer-events-none"
draggable={false}
/>
) : (
<Wrench className="size-3.5 sm:size-4 shrink-0 text-muted-foreground" />
)}
<span className="flex-1 min-w-0 text-xs sm:text-sm font-medium truncate">
{group.label}
</span>
<Switch
checked={!allDisabled}
onCheckedChange={() => toggleToolGroup(toolNames)}
className="shrink-0 scale-[0.6] sm:scale-75"
/>
) : (
<Wrench className="size-3.5 sm:size-4 shrink-0 text-muted-foreground" />
)}
<span className="flex-1 min-w-0 text-xs sm:text-sm font-medium truncate">
{group.label}
</span>
<Switch
checked={!allDisabled}
onCheckedChange={() => toggleToolGroup(toolNames)}
className="shrink-0 scale-[0.6] sm:scale-75"
/>
</div>
);
return (
<Tooltip key={group.label}>
<TooltipTrigger asChild>{row}</TooltipTrigger>
<TooltipContent side="right" className="max-w-72 text-xs">
{groupDef?.tooltip ?? group.tools.map((t) => t.description).join(" · ")}
</TooltipContent>
</Tooltip>
);
})}
</div>
);
return (
<Tooltip key={group.label}>
<TooltipTrigger asChild>{row}</TooltipTrigger>
<TooltipContent side="right" className="max-w-72 text-xs">
{groupDef?.tooltip ??
group.tools.map((t) => t.description).join(" · ")}
</TooltipContent>
</Tooltip>
);
})}
</div>
)}
{!filteredTools?.length && (

View file

@ -78,14 +78,21 @@ export function ComposioDriveFolderTree({
}: ComposioDriveFolderTreeProps) {
const [itemStates, setItemStates] = useState<Map<string, ItemTreeNode>>(new Map());
const { data: rootData, isLoading: isLoadingRoot, error: rootError } = useComposioDriveFolders({
const {
data: rootData,
isLoading: isLoadingRoot,
error: rootError,
} = useComposioDriveFolders({
connectorId,
});
useEffect(() => {
if (rootError && onAuthError) {
const msg = rootError instanceof Error ? rootError.message : String(rootError);
if (msg.toLowerCase().includes("authentication expired") || msg.toLowerCase().includes("re-authenticate")) {
if (
msg.toLowerCase().includes("authentication expired") ||
msg.toLowerCase().includes("re-authenticate")
) {
onAuthError(msg);
}
}
@ -363,19 +370,21 @@ export function ComposioDriveFolderTree({
{!isLoadingRoot && rootItems.map((item) => renderItem(item, 0))}
</div>
{!isLoadingRoot && rootError && (
<div className="text-center text-xs sm:text-sm text-amber-600 dark:text-amber-500 py-4 sm:py-8">
{(rootError instanceof Error ? rootError.message : String(rootError)).includes("authentication expired")
? "Google Drive authentication has expired. Please re-authenticate above."
: "Failed to load Google Drive contents."}
</div>
)}
{!isLoadingRoot && rootError && (
<div className="text-center text-xs sm:text-sm text-amber-600 dark:text-amber-500 py-4 sm:py-8">
{(rootError instanceof Error ? rootError.message : String(rootError)).includes(
"authentication expired"
)
? "Google Drive authentication has expired. Please re-authenticate above."
: "Failed to load Google Drive contents."}
</div>
)}
{!isLoadingRoot && !rootError && rootItems.length === 0 && (
<div className="text-center text-xs sm:text-sm text-muted-foreground py-4 sm:py-8">
No files or folders found in your Google Drive
</div>
)}
{!isLoadingRoot && !rootError && rootItems.length === 0 && (
<div className="text-center text-xs sm:text-sm text-muted-foreground py-4 sm:py-8">
No files or folders found in your Google Drive
</div>
)}
</div>
</ScrollArea>
</div>

View file

@ -1,18 +1,15 @@
"use client";
import { TagInput, type Tag as TagType } from "emblor";
import { format } from "date-fns";
import { TagInput, type Tag as TagType } from "emblor";
import { useAtomValue, useSetAtom } from "jotai";
import { CalendarIcon, XIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
closeHitlEditPanelAtom,
hitlEditPanelAtom,
} from "@/atoms/chat/hitl-edit-panel.atom";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { Calendar } from "@/components/ui/calendar";
import { closeHitlEditPanelAtom, hitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@ -58,14 +55,9 @@ function EmailsTagField({
onChangeRef.current(tagsToEmailString(tags));
}, [tags]);
const handleSetTags = useCallback(
(newTags: TagType[] | ((prev: TagType[]) => TagType[])) => {
setTags((prev) =>
typeof newTags === "function" ? newTags(prev) : newTags
);
},
[]
);
const handleSetTags = useCallback((newTags: TagType[] | ((prev: TagType[]) => TagType[])) => {
setTags((prev) => (typeof newTags === "function" ? newTags(prev) : newTags));
}, []);
const handleAddTag = useCallback(
(text: string) => {
@ -265,7 +257,10 @@ export function HitlEditPanelContent({
<div className="flex flex-col gap-3 px-4 py-3 border-b">
{extraFields.map((field) => (
<div key={field.key} className="flex flex-col gap-1.5">
<Label htmlFor={`extra-field-${field.key}`} className="text-xs font-medium text-muted-foreground">
<Label
htmlFor={`extra-field-${field.key}`}
className="text-xs font-medium text-muted-foreground"
>
{field.label}
</Label>
{field.type === "emails" ? (
@ -360,9 +355,7 @@ function MobileHitlEditDrawer() {
overlayClassName="z-80"
>
<DrawerHandle />
<DrawerTitle className="sr-only">
Edit {panelState.toolName}
</DrawerTitle>
<DrawerTitle className="sr-only">Edit {panelState.toolName}</DrawerTitle>
<div className="min-h-0 flex-1 flex flex-col overflow-hidden">
<HitlEditPanelContent
title={panelState.title}

View file

@ -3,7 +3,6 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { AlertTriangle, Inbox, Megaphone, SquareLibrary } from "lucide-react";
import { Spinner } from "@/components/ui/spinner";
import { useParams, usePathname, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useTheme } from "next-themes";
@ -22,6 +21,10 @@ import {
userSettingsDialogAtom,
} from "@/atoms/settings/settings-dialog.atoms";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { MorePagesDialog } from "@/components/settings/more-pages-dialog";
import { SearchSpaceSettingsDialog } from "@/components/settings/search-space-settings-dialog";
import { TeamDialog } from "@/components/settings/team-dialog";
import { UserSettingsDialog } from "@/components/settings/user-settings-dialog";
import {
AlertDialog,
AlertDialogAction,
@ -42,7 +45,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/spinner";
import { useAnnouncements } from "@/hooks/use-announcements";
import { useDocumentsProcessing } from "@/hooks/use-documents-processing";
import { useInbox } from "@/hooks/use-inbox";
@ -54,10 +57,6 @@ import { deleteThread, fetchThreads, updateThread } from "@/lib/chat/thread-pers
import { cleanupElectric } from "@/lib/electric/client";
import { resetUser, trackLogout } from "@/lib/posthog/events";
import { cacheKeys } from "@/lib/query-client/cache-keys";
import { MorePagesDialog } from "@/components/settings/more-pages-dialog";
import { SearchSpaceSettingsDialog } from "@/components/settings/search-space-settings-dialog";
import { TeamDialog } from "@/components/settings/team-dialog";
import { UserSettingsDialog } from "@/components/settings/user-settings-dialog";
import type { ChatItem, NavItem, SearchSpace } from "../types/layout.types";
import { CreateSearchSpaceDialog } from "../ui/dialogs";
import { LayoutShell } from "../ui/shell";
@ -822,11 +821,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
disabled={isDeletingChat}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 gap-2"
>
{isDeletingChat ? (
<Spinner size="sm" />
) : (
tCommon("delete")
)}
{isDeletingChat ? <Spinner size="sm" /> : tCommon("delete")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>

View file

@ -114,7 +114,13 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
} else if (effectiveTab === "report" && !reportOpen) {
effectiveTab = editorOpen ? "editor" : "sources";
} else if (effectiveTab === "sources" && !documentsOpen) {
effectiveTab = hitlEditOpen ? "hitl-edit" : editorOpen ? "editor" : reportOpen ? "report" : "sources";
effectiveTab = hitlEditOpen
? "hitl-edit"
: editorOpen
? "editor"
: reportOpen
? "report"
: "sources";
}
const targetWidth = PANEL_WIDTHS[effectiveTab];
@ -160,14 +166,14 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
)}
{effectiveTab === "hitl-edit" && hitlEditOpen && hitlEditState.onSave && (
<div className="h-full flex flex-col">
<HitlEditPanelContent
title={hitlEditState.title}
content={hitlEditState.content}
toolName={hitlEditState.toolName}
extraFields={hitlEditState.extraFields}
onSave={hitlEditState.onSave}
onClose={closeHitlEdit}
/>
<HitlEditPanelContent
title={hitlEditState.title}
content={hitlEditState.content}
toolName={hitlEditState.toolName}
extraFields={hitlEditState.extraFields}
onSave={hitlEditState.onSave}
onClose={closeHitlEdit}
/>
</div>
)}
</div>

View file

@ -18,6 +18,7 @@ import { useParams, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
import { Button } from "@/components/ui/button";
import {
Dialog,
@ -36,7 +37,6 @@ import {
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { useLongPress } from "@/hooks/use-long-press";

View file

@ -18,6 +18,7 @@ import { useParams, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
import { Button } from "@/components/ui/button";
import {
Dialog,
@ -36,7 +37,6 @@ import {
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { useLongPress } from "@/hooks/use-long-press";

View file

@ -15,7 +15,6 @@ import {
} from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
createImageGenConfigMutationAtom,
deleteImageGenConfigMutationAtom,
@ -38,6 +37,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
@ -69,12 +69,12 @@ import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { useMediaQuery } from "@/hooks/use-media-query";
import {
getImageGenModelsByProvider,
IMAGE_GEN_PROVIDERS,
} from "@/contracts/enums/image-gen-providers";
import type { ImageGenerationConfig } from "@/contracts/types/new-llm-config.types";
import { useMediaQuery } from "@/hooks/use-media-query";
import { getProviderIcon } from "@/lib/provider-icons";
import { cn } from "@/lib/utils";

View file

@ -13,7 +13,6 @@ import {
Wand2,
} from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
import {
createNewLLMConfigMutationAtom,
@ -36,6 +35,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";

View file

@ -2,9 +2,9 @@
import { useAtom } from "jotai";
import { useTranslations } from "next-intl";
import { TeamContent } from "@/app/dashboard/[search_space_id]/team/team-content";
import { teamDialogAtom } from "@/atoms/settings/settings-dialog.atoms";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { TeamContent } from "@/app/dashboard/[search_space_id]/team/team-content";
interface TeamDialogProps {
searchSpaceId: number;

View file

@ -3,9 +3,9 @@
import { useAtom } from "jotai";
import { KeyRound, User } from "lucide-react";
import { useTranslations } from "next-intl";
import { userSettingsDialogAtom } from "@/atoms/settings/settings-dialog.atoms";
import { ApiKeyContent } from "@/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent";
import { ProfileContent } from "@/app/dashboard/[search_space_id]/user-settings/components/ProfileContent";
import { userSettingsDialogAtom } from "@/atoms/settings/settings-dialog.atoms";
import { SettingsDialog } from "@/components/settings/settings-dialog";
export function UserSettingsDialog() {

View file

@ -1,8 +1,12 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import {
Select,
@ -11,11 +15,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
import { useSetAtom } from "jotai";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
interface ConfluenceAccount {
id: number;
@ -108,9 +108,7 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
);
}
function isInsufficientPermissionsResult(
result: unknown,
): result is InsufficientPermissionsResult {
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
return (
typeof result === "object" &&
result !== null &&
@ -161,7 +159,7 @@ function ApprovalCard({
space_id: selectedSpaceId || null,
};
},
[args.title, args.content, selectedAccountId, selectedSpaceId, pendingEdits],
[args.title, args.content, selectedAccountId, selectedSpaceId, pendingEdits]
);
const handleApprove = useCallback(() => {
@ -177,7 +175,17 @@ function ApprovalCard({
args: buildFinalArgs(),
},
});
}, [phase, setProcessing, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, buildFinalArgs, pendingEdits]);
}, [
phase,
setProcessing,
isPanelOpen,
canApprove,
allowedDecisions,
onDecision,
interruptData,
buildFinalArgs,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -202,15 +210,16 @@ function ApprovalCard({
: "Create Confluence Page"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={pendingEdits ? "Creating page with your changes" : "Creating page"} size="sm" />
<TextShimmerLoader
text={pendingEdits ? "Creating page with your changes" : "Creating page"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits ? "Page created with your changes" : "Page created"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Page creation was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Page creation was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -225,8 +234,8 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
openHitlEditPanel({
title: pendingEdits?.title ?? (args.title ?? ""),
content: pendingEdits?.content ?? (args.content ?? ""),
title: pendingEdits?.title ?? args.title ?? "",
content: pendingEdits?.content ?? args.content ?? "",
toolName: "Confluence Page",
onSave: (newTitle, newContent) => {
setIsPanelOpen(false);
@ -290,10 +299,7 @@ function ApprovalCard({
<p className="text-xs font-medium text-muted-foreground">
Space <span className="text-destructive">*</span>
</p>
<Select
value={selectedSpaceId}
onValueChange={setSelectedSpaceId}
>
<Select value={selectedSpaceId} onValueChange={setSelectedSpaceId}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a space" />
</SelectTrigger>
@ -379,9 +385,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
All Confluence accounts expired
</p>
<p className="text-sm font-semibold text-destructive">All Confluence accounts expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -391,9 +395,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
);
}
function InsufficientPermissionsCard({
result,
}: { result: InsufficientPermissionsResult }) {
function InsufficientPermissionsCard({ result }: { result: InsufficientPermissionsResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
@ -474,7 +476,8 @@ export const CreateConfluencePageToolUI = makeAssistantToolUI<
}
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isInsufficientPermissionsResult(result)) return <InsufficientPermissionsCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;
return <SuccessCard result={result as SuccessResult} />;

View file

@ -3,9 +3,9 @@
import { makeAssistantToolUI } from "@assistant-ui/react";
import { CornerDownLeftIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface InterruptResult {
@ -132,9 +132,7 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
);
}
function isInsufficientPermissionsResult(
result: unknown,
): result is InsufficientPermissionsResult {
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
return (
typeof result === "object" &&
result !== null &&
@ -174,7 +172,15 @@ function ApprovalCard({
},
},
});
}, [phase, setProcessing, onDecision, interruptData, page?.page_id, context?.account?.id, deleteFromKb]);
}, [
phase,
setProcessing,
onDecision,
interruptData,
page?.page_id,
context?.account?.id,
deleteFromKb,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -203,9 +209,7 @@ function ApprovalCard({
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">Page deleted</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Page deletion was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Page deletion was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -238,9 +242,7 @@ function ApprovalCard({
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm space-y-1">
<div className="font-medium">{page.page_title}</div>
{page.space_id && (
<div className="text-xs text-muted-foreground">
Space: {page.space_id}
</div>
<div className="text-xs text-muted-foreground">Space: {page.space_id}</div>
)}
</div>
</div>
@ -279,11 +281,7 @@ function ApprovalCard({
<>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 flex items-center gap-2 select-none">
<Button
size="sm"
className="rounded-lg gap-1.5"
onClick={handleApprove}
>
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
Approve
<CornerDownLeftIcon className="size-3 opacity-60" />
</Button>
@ -309,9 +307,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Confluence authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Confluence authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -321,9 +317,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
);
}
function InsufficientPermissionsCard({
result,
}: { result: InsufficientPermissionsResult }) {
function InsufficientPermissionsCard({ result }: { result: InsufficientPermissionsResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
@ -357,9 +351,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
Page not found
</p>
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Page not found</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -437,7 +429,8 @@ export const DeleteConfluencePageToolUI = makeAssistantToolUI<
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isInsufficientPermissionsResult(result)) return <InsufficientPermissionsCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isWarningResult(result)) return <WarningCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;

View file

@ -4,11 +4,11 @@ import { makeAssistantToolUI } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
interface InterruptResult {
__interrupt__: true;
@ -116,9 +116,7 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
);
}
function isInsufficientPermissionsResult(
result: unknown,
): result is InsufficientPermissionsResult {
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
return (
typeof result === "object" &&
result !== null &&
@ -169,8 +167,7 @@ function ApprovalCard({
const canEdit = allowedDecisions.includes("edit");
const hasProposedChanges =
actionArgs.new_title || args.new_title ||
actionArgs.new_content || args.new_content;
actionArgs.new_title || args.new_title || actionArgs.new_content || args.new_content;
const buildFinalArgs = useCallback(() => {
return {
@ -196,7 +193,16 @@ function ApprovalCard({
args: buildFinalArgs(),
},
});
}, [phase, setProcessing, isPanelOpen, allowedDecisions, onDecision, interruptData, buildFinalArgs, hasPanelEdits]);
}, [
phase,
setProcessing,
isPanelOpen,
allowedDecisions,
onDecision,
interruptData,
buildFinalArgs,
hasPanelEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -221,15 +227,16 @@ function ApprovalCard({
: "Update Confluence Page"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={hasPanelEdits ? "Updating page with your changes" : "Updating page"} size="sm" />
<TextShimmerLoader
text={hasPanelEdits ? "Updating page with your changes" : "Updating page"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{hasPanelEdits ? "Page updated with your changes" : "Page updated"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Page update was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Page update was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -293,7 +300,8 @@ function ApprovalCard({
className="max-h-[5rem] overflow-hidden text-xs text-muted-foreground"
style={{
maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(to bottom, black 50%, transparent 100%)",
}}
>
<PlateEditor
@ -306,9 +314,7 @@ function ApprovalCard({
</div>
)}
{page.space_id && (
<div className="text-xs text-muted-foreground">
Space: {page.space_id}
</div>
<div className="text-xs text-muted-foreground">Space: {page.space_id}</div>
)}
</div>
</div>
@ -322,14 +328,18 @@ function ApprovalCard({
{/* Content preview — proposed changes */}
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3">
{(hasProposedChanges || hasPanelEdits) ? (
{hasProposedChanges || hasPanelEdits ? (
<>
{(hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title)) && (
<p className="text-sm font-medium text-foreground">
{String(hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title))}
{String(
hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title)
)}
</p>
)}
{(hasPanelEdits ? editedArgs.content : (actionArgs.new_content ?? args.new_content)) && (
{(hasPanelEdits
? editedArgs.content
: (actionArgs.new_content ?? args.new_content)) && (
<div
className="max-h-[7rem] overflow-hidden text-sm"
style={{
@ -338,7 +348,11 @@ function ApprovalCard({
}}
>
<PlateEditor
markdown={String(hasPanelEdits ? editedArgs.content : (actionArgs.new_content ?? args.new_content))}
markdown={String(
hasPanelEdits
? editedArgs.content
: (actionArgs.new_content ?? args.new_content)
)}
readOnly
preset="readonly"
editorVariant="none"
@ -393,9 +407,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Confluence authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Confluence authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -405,9 +417,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
);
}
function InsufficientPermissionsCard({
result,
}: { result: InsufficientPermissionsResult }) {
function InsufficientPermissionsCard({ result }: { result: InsufficientPermissionsResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
@ -441,9 +451,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
Page not found
</p>
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Page not found</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -509,7 +517,8 @@ export const UpdateConfluencePageToolUI = makeAssistantToolUI<
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isInsufficientPermissionsResult(result)) return <InsufficientPermissionsCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;
return <SuccessCard result={result as SuccessResult} />;

View file

@ -1,13 +1,13 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
CornerDownLeftIcon,
Pen,
UserIcon,
UsersIcon,
} from "lucide-react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, Pen, UserIcon, UsersIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import {
Select,
@ -16,11 +16,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useSetAtom } from "jotai";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface GmailAccount {
@ -132,7 +127,11 @@ function ApprovalCard({
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{
subject: string; body: string; to: string; cc: string; bcc: string;
subject: string;
body: string;
to: string;
cc: string;
bcc: string;
} | null>(null);
const accounts = interruptData.context?.accounts ?? [];
@ -175,7 +174,18 @@ function ApprovalCard({
},
},
});
}, [phase, isPanelOpen, canApprove, allowedDecisions, setProcessing, onDecision, interruptData, args, selectedAccountId, pendingEdits]);
}, [
phase,
isPanelOpen,
canApprove,
allowedDecisions,
setProcessing,
onDecision,
interruptData,
args,
selectedAccountId,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -200,16 +210,17 @@ function ApprovalCard({
? "Gmail Draft Approved"
: "Create Gmail Draft"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={pendingEdits ? "Creating draft with your changes" : "Creating draft"} size="sm" />
{phase === "processing" ? (
<TextShimmerLoader
text={pendingEdits ? "Creating draft with your changes" : "Creating draft"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits ? "Draft created with your changes" : "Draft created"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Draft creation was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Draft creation was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -225,13 +236,28 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
const extraFields: ExtraField[] = [
{ key: "to", label: "To", type: "emails", value: pendingEdits?.to ?? args.to ?? "" },
{ key: "cc", label: "CC", type: "emails", value: pendingEdits?.cc ?? args.cc ?? "" },
{ key: "bcc", label: "BCC", type: "emails", value: pendingEdits?.bcc ?? args.bcc ?? "" },
{
key: "to",
label: "To",
type: "emails",
value: pendingEdits?.to ?? args.to ?? "",
},
{
key: "cc",
label: "CC",
type: "emails",
value: pendingEdits?.cc ?? args.cc ?? "",
},
{
key: "bcc",
label: "BCC",
type: "emails",
value: pendingEdits?.bcc ?? args.bcc ?? "",
},
];
openHitlEditPanel({
title: pendingEdits?.subject ?? (args.subject ?? ""),
content: pendingEdits?.body ?? (args.body ?? ""),
title: pendingEdits?.subject ?? args.subject ?? "",
content: pendingEdits?.body ?? args.body ?? "",
toolName: "Gmail Draft",
extraFields,
onSave: (newTitle, newContent, extraFieldValues) => {
@ -322,7 +348,9 @@ function ApprovalCard({
<div className="px-5 pt-1">
{(pendingEdits?.subject ?? args.subject) != null && (
<p className="text-sm font-medium text-foreground">{pendingEdits?.subject ?? args.subject}</p>
<p className="text-sm font-medium text-foreground">
{pendingEdits?.subject ?? args.subject}
</p>
)}
{(pendingEdits?.body ?? args.body) != null && (
<div
@ -398,9 +426,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Gmail authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Gmail authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -1,14 +1,13 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
CornerDownLeftIcon,
MailIcon,
Pen,
UserIcon,
UsersIcon,
} from "lucide-react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, MailIcon, Pen, UserIcon, UsersIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import {
Select,
@ -17,11 +16,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useSetAtom } from "jotai";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface GmailAccount {
@ -132,7 +126,11 @@ function ApprovalCard({
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{
subject: string; body: string; to: string; cc: string; bcc: string;
subject: string;
body: string;
to: string;
cc: string;
bcc: string;
} | null>(null);
const accounts = interruptData.context?.accounts ?? [];
@ -175,7 +173,18 @@ function ApprovalCard({
},
},
});
}, [phase, isPanelOpen, canApprove, allowedDecisions, setProcessing, onDecision, interruptData, args, selectedAccountId, pendingEdits]);
}, [
phase,
isPanelOpen,
canApprove,
allowedDecisions,
setProcessing,
onDecision,
interruptData,
args,
selectedAccountId,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -200,16 +209,17 @@ function ApprovalCard({
? "Email Sending Approved"
: "Send Email"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={pendingEdits ? "Sending email with your changes" : "Sending email"} size="sm" />
{phase === "processing" ? (
<TextShimmerLoader
text={pendingEdits ? "Sending email with your changes" : "Sending email"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits ? "Email sent with your changes" : "Email sent"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Email sending was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Email sending was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -225,13 +235,28 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
const extraFields: ExtraField[] = [
{ key: "to", label: "To", type: "emails", value: pendingEdits?.to ?? args.to ?? "" },
{ key: "cc", label: "CC", type: "emails", value: pendingEdits?.cc ?? args.cc ?? "" },
{ key: "bcc", label: "BCC", type: "emails", value: pendingEdits?.bcc ?? args.bcc ?? "" },
{
key: "to",
label: "To",
type: "emails",
value: pendingEdits?.to ?? args.to ?? "",
},
{
key: "cc",
label: "CC",
type: "emails",
value: pendingEdits?.cc ?? args.cc ?? "",
},
{
key: "bcc",
label: "BCC",
type: "emails",
value: pendingEdits?.bcc ?? args.bcc ?? "",
},
];
openHitlEditPanel({
title: pendingEdits?.subject ?? (args.subject ?? ""),
content: pendingEdits?.body ?? (args.body ?? ""),
title: pendingEdits?.subject ?? args.subject ?? "",
content: pendingEdits?.body ?? args.body ?? "",
toolName: "Send Email",
extraFields,
onSave: (newTitle, newContent, extraFieldValues) => {
@ -264,32 +289,32 @@ function ApprovalCard({
<p className="text-sm text-destructive">{interruptData.context.error}</p>
) : (
accounts.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Gmail Account <span className="text-destructive">*</span>
</p>
<Select value={selectedAccountId} onValueChange={setSelectedAccountId}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an account" />
</SelectTrigger>
<SelectContent>
{validAccounts.map((account) => (
<SelectItem key={account.id} value={String(account.id)}>
{account.name}
</SelectItem>
))}
{expiredAccounts.map((a) => (
<div
key={a.id}
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
>
{a.name} (expired, retry after re-auth)
</div>
))}
</SelectContent>
</Select>
</div>
)
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Gmail Account <span className="text-destructive">*</span>
</p>
<Select value={selectedAccountId} onValueChange={setSelectedAccountId}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an account" />
</SelectTrigger>
<SelectContent>
{validAccounts.map((account) => (
<SelectItem key={account.id} value={String(account.id)}>
{account.name}
</SelectItem>
))}
{expiredAccounts.map((a) => (
<div
key={a.id}
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
>
{a.name} (expired, retry after re-auth)
</div>
))}
</SelectContent>
</Select>
</div>
)
)}
</div>
</>
@ -320,7 +345,9 @@ function ApprovalCard({
<div className="px-5 pt-1">
{(pendingEdits?.subject ?? args.subject) != null && (
<p className="text-sm font-medium text-foreground">{pendingEdits?.subject ?? args.subject}</p>
<p className="text-sm font-medium text-foreground">
{pendingEdits?.subject ?? args.subject}
</p>
)}
{(pendingEdits?.body ?? args.body) != null && (
<div
@ -396,9 +423,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Gmail authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Gmail authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -1,16 +1,11 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
CalendarIcon,
CornerDownLeftIcon,
MailIcon,
UserIcon,
} from "lucide-react";
import { CalendarIcon, CornerDownLeftIcon, MailIcon, UserIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface GmailAccount {
@ -192,14 +187,12 @@ function ApprovalCard({
? "Email Trash Approved"
: "Trash Email"}
</p>
{phase === "processing" ? (
{phase === "processing" ? (
<TextShimmerLoader text="Trashing email" size="sm" />
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">Email trashed</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Email trash was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Email trash was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -280,11 +273,7 @@ function ApprovalCard({
<>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 flex items-center gap-2 select-none">
<Button
size="sm"
className="rounded-lg gap-1.5"
onClick={handleApprove}
>
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
Approve
<CornerDownLeftIcon className="size-3 opacity-60" />
</Button>
@ -324,9 +313,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Gmail authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Gmail authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -1,20 +1,14 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
CornerDownLeftIcon,
MailIcon,
Pen,
UserIcon,
UsersIcon,
} from "lucide-react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, MailIcon, Pen, UserIcon, UsersIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useSetAtom } from "jotai";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { Button } from "@/components/ui/button";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface GmailAccount {
@ -127,15 +121,12 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
);
}
function isInsufficientPermissionsResult(
result: unknown,
): result is InsufficientPermissionsResult {
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
return (
typeof result === "object" &&
result !== null &&
"status" in result &&
(result as InsufficientPermissionsResult).status ===
"insufficient_permissions"
(result as InsufficientPermissionsResult).status === "insufficient_permissions"
);
}
@ -177,17 +168,11 @@ function ApprovalCard({
const existingBody = context?.existing_body;
const reviewConfig = interruptData.review_configs?.[0];
const allowedDecisions = reviewConfig?.allowed_decisions ?? [
"approve",
"reject",
];
const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"];
const canEdit = allowedDecisions.includes("edit");
const currentSubject =
pendingEdits?.subject ??
args.subject ??
email?.subject ??
args.draft_subject_or_id;
pendingEdits?.subject ?? args.subject ?? email?.subject ?? args.draft_subject_or_id;
const currentBody = pendingEdits?.body ?? args.body;
const currentTo = pendingEdits?.to ?? args.to ?? "";
const currentCc = pendingEdits?.cc ?? args.cc ?? "";
@ -259,23 +244,15 @@ function ApprovalCard({
</p>
{phase === "processing" ? (
<TextShimmerLoader
text={
pendingEdits
? "Updating draft with your changes"
: "Updating draft"
}
text={pendingEdits ? "Updating draft with your changes" : "Updating draft"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits
? "Draft updated with your changes"
: "Draft updated"}
{pendingEdits ? "Draft updated with your changes" : "Draft updated"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Draft update was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Draft update was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -310,16 +287,12 @@ function ApprovalCard({
value: currentBcc,
},
];
openHitlEditPanel({
title: currentSubject,
content: editableBody,
toolName: "Gmail Draft",
openHitlEditPanel({
title: currentSubject,
content: editableBody,
toolName: "Gmail Draft",
extraFields,
onSave: (
newTitle,
newContent,
extraFieldValues,
) => {
onSave: (newTitle, newContent, extraFieldValues) => {
setIsPanelOpen(false);
const extras = extraFieldValues ?? {};
setPendingEdits({
@ -346,16 +319,12 @@ function ApprovalCard({
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 space-y-4 select-none">
{context.error ? (
<p className="text-sm text-destructive">
{context.error}
</p>
<p className="text-sm text-destructive">{context.error}</p>
) : (
<>
{account && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Gmail Account
</p>
<p className="text-xs font-medium text-muted-foreground">Gmail Account</p>
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm">
{account.name}
</div>
@ -364,15 +333,11 @@ function ApprovalCard({
{email && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Draft to Update
</p>
<p className="text-xs font-medium text-muted-foreground">Draft to Update</p>
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm space-y-1">
<div className="flex items-center gap-1.5">
<MailIcon className="size-3 shrink-0 text-muted-foreground" />
<span className="font-medium">
{email.subject}
</span>
<span className="font-medium">{email.subject}</span>
</div>
</div>
</div>
@ -408,18 +373,14 @@ function ApprovalCard({
<div className="px-5 pt-1">
{currentSubject != null && (
<p className="text-sm font-medium text-foreground">
{currentSubject}
</p>
<p className="text-sm font-medium text-foreground">{currentSubject}</p>
)}
{editableBody ? (
<div
{editableBody ? (
<div
className="mt-2 max-h-[7rem] overflow-hidden text-sm"
style={{
maskImage:
"linear-gradient(to bottom, black 50%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(to bottom, black 50%, transparent 100%)",
maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
}}
>
<PlateEditor
@ -477,9 +438,7 @@ function ErrorCard({ result }: { result: ErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Failed to update Gmail draft
</p>
<p className="text-sm font-semibold text-destructive">Failed to update Gmail draft</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -493,9 +452,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Gmail authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Gmail authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -505,9 +462,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
);
}
function InsufficientPermissionsCard({
result,
}: { result: InsufficientPermissionsResult }) {
function InsufficientPermissionsCard({ result }: { result: InsufficientPermissionsResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
@ -577,7 +532,7 @@ export const UpdateGmailDraftToolUI = makeAssistantToolUI<
window.dispatchEvent(
new CustomEvent("hitl-decision", {
detail: { decisions: [decision] },
}),
})
);
}}
/>

View file

@ -1,16 +1,13 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
ClockIcon,
MapPinIcon,
UsersIcon,
GlobeIcon,
CornerDownLeftIcon,
Pen,
} from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useSetAtom } from "jotai";
import { ClockIcon, CornerDownLeftIcon, GlobeIcon, MapPinIcon, Pen, UsersIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import {
Select,
@ -19,11 +16,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
interface GoogleCalendarAccount {
id: number;
@ -160,8 +153,12 @@ function ApprovalCard({
const [wasEdited, setWasEdited] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{
summary: string; description: string; start_datetime: string;
end_datetime: string; location: string; attendees: string;
summary: string;
description: string;
start_datetime: string;
end_datetime: string;
location: string;
attendees: string;
} | null>(null);
const accounts = interruptData.context?.accounts ?? [];
@ -236,7 +233,19 @@ function ApprovalCard({
args: finalArgs,
},
});
}, [phase, isPanelOpen, canApprove, allowedDecisions, setProcessing, onDecision, interruptData, args, selectedAccountId, selectedCalendarId, pendingEdits]);
}, [
phase,
isPanelOpen,
canApprove,
allowedDecisions,
setProcessing,
onDecision,
interruptData,
args,
selectedAccountId,
selectedCalendarId,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -250,7 +259,10 @@ function ApprovalCard({
const attendeesList = (args.attendees as string[]) ?? [];
const displayAttendees = pendingEdits?.attendees
? pendingEdits.attendees.split(",").map((e) => e.trim()).filter(Boolean)
? pendingEdits.attendees
.split(",")
.map((e) => e.trim())
.filter(Boolean)
: attendeesList;
return (
@ -266,16 +278,17 @@ function ApprovalCard({
? "Calendar Event Approved"
: "Create Calendar Event"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={wasEdited ? "Creating event with your changes" : "Creating event"} size="sm" />
{phase === "processing" ? (
<TextShimmerLoader
text={wasEdited ? "Creating event with your changes" : "Creating event"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{wasEdited ? "Event created with your changes" : "Event created"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Event creation was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Event creation was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -291,14 +304,34 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
const extraFields: ExtraField[] = [
{ key: "start_datetime", label: "Start", type: "datetime-local", value: pendingEdits?.start_datetime ?? args.start_datetime ?? "" },
{ key: "end_datetime", label: "End", type: "datetime-local", value: pendingEdits?.end_datetime ?? args.end_datetime ?? "" },
{ key: "location", label: "Location", type: "text", value: pendingEdits?.location ?? args.location ?? "" },
{ key: "attendees", label: "Attendees", type: "emails", value: pendingEdits?.attendees ?? attendeesList.join(", ") },
{
key: "start_datetime",
label: "Start",
type: "datetime-local",
value: pendingEdits?.start_datetime ?? args.start_datetime ?? "",
},
{
key: "end_datetime",
label: "End",
type: "datetime-local",
value: pendingEdits?.end_datetime ?? args.end_datetime ?? "",
},
{
key: "location",
label: "Location",
type: "text",
value: pendingEdits?.location ?? args.location ?? "",
},
{
key: "attendees",
label: "Attendees",
type: "emails",
value: pendingEdits?.attendees ?? attendeesList.join(", "),
},
];
openHitlEditPanel({
title: pendingEdits?.summary ?? (args.summary ?? ""),
content: pendingEdits?.description ?? (args.description ?? ""),
title: pendingEdits?.summary ?? args.summary ?? "",
content: pendingEdits?.description ?? args.description ?? "",
toolName: "Calendar Event",
extraFields,
onSave: (newTitle, newContent, extraFieldValues) => {
@ -307,10 +340,16 @@ function ApprovalCard({
setPendingEdits({
summary: newTitle,
description: newContent,
start_datetime: extras.start_datetime ?? pendingEdits?.start_datetime ?? args.start_datetime ?? "",
end_datetime: extras.end_datetime ?? pendingEdits?.end_datetime ?? args.end_datetime ?? "",
start_datetime:
extras.start_datetime ??
pendingEdits?.start_datetime ??
args.start_datetime ??
"",
end_datetime:
extras.end_datetime ?? pendingEdits?.end_datetime ?? args.end_datetime ?? "",
location: extras.location ?? pendingEdits?.location ?? args.location ?? "",
attendees: extras.attendees ?? pendingEdits?.attendees ?? attendeesList.join(", "),
attendees:
extras.attendees ?? pendingEdits?.attendees ?? attendeesList.join(", "),
});
},
onClose: () => setIsPanelOpen(false),
@ -372,7 +411,8 @@ function ApprovalCard({
<SelectContent>
{calendars.map((cal) => (
<SelectItem key={cal.id} value={cal.id}>
{cal.summary}{cal.primary ? " (primary)" : ""}
{cal.summary}
{cal.primary ? " (primary)" : ""}
</SelectItem>
))}
</SelectContent>
@ -399,16 +439,26 @@ function ApprovalCard({
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3 pb-3 space-y-2">
{(pendingEdits?.summary ?? args.summary) && (
<p className="text-sm font-medium text-foreground">{pendingEdits?.summary ?? args.summary}</p>
<p className="text-sm font-medium text-foreground">
{pendingEdits?.summary ?? args.summary}
</p>
)}
{((pendingEdits?.start_datetime ?? args.start_datetime) || (pendingEdits?.end_datetime ?? args.end_datetime)) && (
{((pendingEdits?.start_datetime ?? args.start_datetime) ||
(pendingEdits?.end_datetime ?? args.end_datetime)) && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<ClockIcon className="size-3.5 shrink-0" />
<span>
{(pendingEdits?.start_datetime ?? args.start_datetime) ? formatDateTime(pendingEdits?.start_datetime ?? args.start_datetime) : ""}
{(pendingEdits?.start_datetime ?? args.start_datetime) && (pendingEdits?.end_datetime ?? args.end_datetime) ? " — " : ""}
{(pendingEdits?.end_datetime ?? args.end_datetime) ? formatDateTime(pendingEdits?.end_datetime ?? args.end_datetime) : ""}
{(pendingEdits?.start_datetime ?? args.start_datetime)
? formatDateTime(pendingEdits?.start_datetime ?? args.start_datetime)
: ""}
{(pendingEdits?.start_datetime ?? args.start_datetime) &&
(pendingEdits?.end_datetime ?? args.end_datetime)
? " — "
: ""}
{(pendingEdits?.end_datetime ?? args.end_datetime)
? formatDateTime(pendingEdits?.end_datetime ?? args.end_datetime)
: ""}
</span>
</div>
)}

View file

@ -1,16 +1,11 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
CalendarIcon,
ClockIcon,
MapPinIcon,
CornerDownLeftIcon,
} from "lucide-react";
import { CalendarIcon, ClockIcon, CornerDownLeftIcon, MapPinIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface GoogleCalendarAccount {
@ -220,14 +215,12 @@ function ApprovalCard({
? "Calendar Event Deletion Approved"
: "Delete Calendar Event"}
</p>
{phase === "processing" ? (
{phase === "processing" ? (
<TextShimmerLoader text="Deleting event" size="sm" />
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">Event deleted</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Event deletion was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Event deletion was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -247,7 +240,9 @@ function ApprovalCard({
<>
{account && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">Google Calendar Account</p>
<p className="text-xs font-medium text-muted-foreground">
Google Calendar Account
</p>
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm">
{account.name}
</div>
@ -315,11 +310,7 @@ function ApprovalCard({
<>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 flex items-center gap-2 select-none">
<Button
size="sm"
className="rounded-lg gap-1.5"
onClick={handleApprove}
>
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
Approve
<CornerDownLeftIcon className="size-3 opacity-60" />
</Button>

View file

@ -1,3 +1,3 @@
export { CreateCalendarEventToolUI } from "./create-event";
export { UpdateCalendarEventToolUI } from "./update-event";
export { DeleteCalendarEventToolUI } from "./delete-event";
export { UpdateCalendarEventToolUI } from "./update-event";

View file

@ -1,22 +1,22 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
import {
ClockIcon,
MapPinIcon,
UsersIcon,
ArrowRightIcon,
ClockIcon,
CornerDownLeftIcon,
MapPinIcon,
Pen,
UsersIcon,
} from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useSetAtom } from "jotai";
import { Button } from "@/components/ui/button";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
interface GoogleCalendarAccount {
id: number;
@ -180,8 +180,12 @@ function ApprovalCard({
const [wasEdited, setWasEdited] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{
summary: string; description: string; start_datetime: string;
end_datetime: string; location: string; attendees: string;
summary: string;
description: string;
start_datetime: string;
end_datetime: string;
location: string;
attendees: string;
} | null>(null);
const reviewConfig = interruptData.review_configs[0];
@ -196,19 +200,21 @@ function ApprovalCard({
const effectiveNewSummary = actionArgs.new_summary ?? args.new_summary;
const effectiveNewStartDatetime = actionArgs.new_start_datetime ?? args.new_start_datetime;
const effectiveNewEndDatetime = actionArgs.new_end_datetime ?? args.new_end_datetime;
const effectiveNewLocation = actionArgs.new_location !== undefined
? actionArgs.new_location
: args.new_location;
const effectiveNewAttendees = proposedAttendees
?? (Array.isArray(args.new_attendees) ? args.new_attendees : null);
const effectiveNewDescription = actionArgs.new_description !== undefined
? actionArgs.new_description
: args.new_description;
const effectiveNewLocation =
actionArgs.new_location !== undefined ? actionArgs.new_location : args.new_location;
const effectiveNewAttendees =
proposedAttendees ?? (Array.isArray(args.new_attendees) ? args.new_attendees : null);
const effectiveNewDescription =
actionArgs.new_description !== undefined ? actionArgs.new_description : args.new_description;
const changes: Array<{ label: string; oldVal: string; newVal: string }> = [];
if (effectiveNewSummary && String(effectiveNewSummary) !== (event?.summary ?? "")) {
changes.push({ label: "Summary", oldVal: event?.summary ?? "", newVal: String(effectiveNewSummary) });
changes.push({
label: "Summary",
oldVal: event?.summary ?? "",
newVal: String(effectiveNewSummary),
});
}
if (effectiveNewStartDatetime && String(effectiveNewStartDatetime) !== (event?.start ?? "")) {
changes.push({
@ -224,8 +230,15 @@ function ApprovalCard({
newVal: formatDateTime(String(effectiveNewEndDatetime)),
});
}
if (effectiveNewLocation !== undefined && String(effectiveNewLocation ?? "") !== (event?.location ?? "")) {
changes.push({ label: "Location", oldVal: event?.location ?? "", newVal: String(effectiveNewLocation ?? "") });
if (
effectiveNewLocation !== undefined &&
String(effectiveNewLocation ?? "") !== (event?.location ?? "")
) {
changes.push({
label: "Location",
oldVal: event?.location ?? "",
newVal: String(effectiveNewLocation ?? ""),
});
}
if (effectiveNewAttendees) {
const oldStr = currentAttendees.join(", ");
@ -242,7 +255,10 @@ function ApprovalCard({
const buildFinalArgs = useCallback(() => {
if (pendingEdits) {
const attendeesArr = pendingEdits.attendees
? pendingEdits.attendees.split(",").map((e) => e.trim()).filter(Boolean)
? pendingEdits.attendees
.split(",")
.map((e) => e.trim())
.filter(Boolean)
: null;
return {
event_id: event?.event_id,
@ -282,7 +298,16 @@ function ApprovalCard({
args: buildFinalArgs(),
},
});
}, [phase, isPanelOpen, allowedDecisions, setProcessing, onDecision, interruptData, buildFinalArgs, pendingEdits]);
}, [
phase,
isPanelOpen,
allowedDecisions,
setProcessing,
onDecision,
interruptData,
buildFinalArgs,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -308,15 +333,16 @@ function ApprovalCard({
: "Update Calendar Event"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={wasEdited ? "Updating event with your changes" : "Updating event"} size="sm" />
<TextShimmerLoader
text={wasEdited ? "Updating event with your changes" : "Updating event"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{wasEdited ? "Event updated with your changes" : "Event updated"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Event update was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Event update was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -331,24 +357,48 @@ function ApprovalCard({
className="rounded-lg text-muted-foreground -mt-1 -mr-2"
onClick={() => {
setIsPanelOpen(true);
const proposedSummary = pendingEdits?.summary
?? (actionArgs.new_summary ? String(actionArgs.new_summary) : (event?.summary ?? ""));
const proposedDescription = pendingEdits?.description
?? (actionArgs.new_description ? String(actionArgs.new_description) : (event?.description ?? ""));
const proposedStart = pendingEdits?.start_datetime
?? (actionArgs.new_start_datetime ? String(actionArgs.new_start_datetime) : (event?.start ?? ""));
const proposedEnd = pendingEdits?.end_datetime
?? (actionArgs.new_end_datetime ? String(actionArgs.new_end_datetime) : (event?.end ?? ""));
const proposedLocation = pendingEdits?.location
?? (actionArgs.new_location !== undefined ? String(actionArgs.new_location ?? "") : (event?.location ?? ""));
const proposedAttendeesStr = pendingEdits?.attendees
?? (proposedAttendees ? proposedAttendees.join(", ") : currentAttendees.join(", "));
const proposedSummary =
pendingEdits?.summary ??
(actionArgs.new_summary ? String(actionArgs.new_summary) : (event?.summary ?? ""));
const proposedDescription =
pendingEdits?.description ??
(actionArgs.new_description
? String(actionArgs.new_description)
: (event?.description ?? ""));
const proposedStart =
pendingEdits?.start_datetime ??
(actionArgs.new_start_datetime
? String(actionArgs.new_start_datetime)
: (event?.start ?? ""));
const proposedEnd =
pendingEdits?.end_datetime ??
(actionArgs.new_end_datetime
? String(actionArgs.new_end_datetime)
: (event?.end ?? ""));
const proposedLocation =
pendingEdits?.location ??
(actionArgs.new_location !== undefined
? String(actionArgs.new_location ?? "")
: (event?.location ?? ""));
const proposedAttendeesStr =
pendingEdits?.attendees ??
(proposedAttendees ? proposedAttendees.join(", ") : currentAttendees.join(", "));
const extraFields: ExtraField[] = [
{ key: "start_datetime", label: "Start", type: "datetime-local", value: proposedStart },
{
key: "start_datetime",
label: "Start",
type: "datetime-local",
value: proposedStart,
},
{ key: "end_datetime", label: "End", type: "datetime-local", value: proposedEnd },
{ key: "location", label: "Location", type: "text", value: proposedLocation },
{ key: "attendees", label: "Attendees", type: "emails", value: proposedAttendeesStr },
{
key: "attendees",
label: "Attendees",
type: "emails",
value: proposedAttendeesStr,
},
];
openHitlEditPanel({
title: proposedSummary,
@ -377,7 +427,7 @@ function ApprovalCard({
)}
</div>
{/* Content section */}
{/* Content section */}
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 space-y-4 select-none">
{context?.error ? (
@ -433,9 +483,13 @@ function ApprovalCard({
<div key={change.label} className="text-xs space-y-0.5">
<span className="text-muted-foreground">{change.label}</span>
<div className="flex items-center gap-1.5 flex-wrap">
<span className="text-muted-foreground line-through">{change.oldVal || "(empty)"}</span>
<span className="text-muted-foreground line-through">
{change.oldVal || "(empty)"}
</span>
<ArrowRightIcon className="size-3 text-muted-foreground shrink-0" />
<span className="font-medium text-foreground">{change.newVal || "(empty)"}</span>
<span className="font-medium text-foreground">
{change.newVal || "(empty)"}
</span>
</div>
</div>
))}
@ -446,7 +500,8 @@ function ApprovalCard({
className="mt-1 max-h-[5rem] overflow-hidden"
style={{
maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(to bottom, black 50%, transparent 100%)",
}}
>
<PlateEditor

View file

@ -1,12 +1,12 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
CornerDownLeftIcon,
FileIcon,
Pen,
} from "lucide-react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, FileIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import {
Select,
@ -15,11 +15,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
import { useSetAtom } from "jotai";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
interface GoogleDriveAccount {
id: number;
@ -139,8 +135,8 @@ function ApprovalCard({
const [pendingEdits, setPendingEdits] = useState<{ name: string; content: string } | null>(null);
const accounts = interruptData.context?.accounts ?? [];
const validAccounts = accounts.filter(a => !a.auth_expired);
const expiredAccounts = accounts.filter(a => a.auth_expired);
const validAccounts = accounts.filter((a) => !a.auth_expired);
const expiredAccounts = accounts.filter((a) => a.auth_expired);
const defaultAccountId = useMemo(() => {
if (validAccounts.length === 1) return String(validAccounts[0].id);
@ -162,7 +158,8 @@ function ApprovalCard({
setParentFolderId("__root__");
}, []);
const fileTypeLabel = FILE_TYPE_LABELS[selectedFileType] ?? FILE_TYPE_LABELS[args.file_type] ?? "Google Drive File";
const fileTypeLabel =
FILE_TYPE_LABELS[selectedFileType] ?? FILE_TYPE_LABELS[args.file_type] ?? "Google Drive File";
const isNameValid = useMemo(() => {
const name = pendingEdits?.name ?? args.name;
@ -194,7 +191,20 @@ function ApprovalCard({
},
},
});
}, [phase, setProcessing, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, args, selectedFileType, selectedAccountId, parentFolderId, pendingEdits]);
}, [
phase,
setProcessing,
isPanelOpen,
canApprove,
allowedDecisions,
onDecision,
interruptData,
args,
selectedFileType,
selectedAccountId,
parentFolderId,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -218,16 +228,17 @@ function ApprovalCard({
? `${fileTypeLabel} Approved`
: `Create ${fileTypeLabel}`}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={pendingEdits ? "Creating file with your changes" : "Creating file"} size="sm" />
{phase === "processing" ? (
<TextShimmerLoader
text={pendingEdits ? "Creating file with your changes" : "Creating file"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits ? "File created with your changes" : "File created"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
File creation was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">File creation was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -242,8 +253,8 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
openHitlEditPanel({
title: pendingEdits?.name ?? (args.name ?? ""),
content: pendingEdits?.content ?? (args.content ?? ""),
title: pendingEdits?.name ?? args.name ?? "",
content: pendingEdits?.content ?? args.content ?? "",
toolName: fileTypeLabel,
onSave: (newName, newContent) => {
setIsPanelOpen(false);
@ -268,33 +279,33 @@ function ApprovalCard({
<p className="text-sm text-destructive">{interruptData.context.error}</p>
) : (
<>
{accounts.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Google Drive Account <span className="text-destructive">*</span>
</p>
<Select value={selectedAccountId} onValueChange={handleAccountChange}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an account" />
</SelectTrigger>
<SelectContent>
{validAccounts.map((account) => (
<SelectItem key={account.id} value={String(account.id)}>
{account.name}
</SelectItem>
))}
{expiredAccounts.map((a) => (
<div
key={a.id}
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
>
{a.name} (expired, retry after re-auth)
</div>
))}
</SelectContent>
</Select>
</div>
)}
{accounts.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Google Drive Account <span className="text-destructive">*</span>
</p>
<Select value={selectedAccountId} onValueChange={handleAccountChange}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an account" />
</SelectTrigger>
<SelectContent>
{validAccounts.map((account) => (
<SelectItem key={account.id} value={String(account.id)}>
{account.name}
</SelectItem>
))}
{expiredAccounts.map((a) => (
<div
key={a.id}
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
>
{a.name} (expired, retry after re-auth)
</div>
))}
</SelectContent>
</Select>
</div>
)}
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
@ -311,31 +322,29 @@ function ApprovalCard({
</Select>
</div>
{selectedAccountId && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Parent Folder
</p>
<Select value={parentFolderId} onValueChange={setParentFolderId}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Drive Root" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__root__">Drive Root</SelectItem>
{availableParentFolders.map((folder) => (
<SelectItem key={folder.folder_id} value={folder.folder_id}>
{folder.name}
</SelectItem>
))}
</SelectContent>
</Select>
{availableParentFolders.length === 0 && (
<p className="text-xs text-muted-foreground">
No folders found. File will be created at Drive root.
</p>
)}
</div>
)}
{selectedAccountId && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">Parent Folder</p>
<Select value={parentFolderId} onValueChange={setParentFolderId}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Drive Root" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__root__">Drive Root</SelectItem>
{availableParentFolders.map((folder) => (
<SelectItem key={folder.folder_id} value={folder.folder_id}>
{folder.name}
</SelectItem>
))}
</SelectContent>
</Select>
{availableParentFolders.length === 0 && (
<p className="text-xs text-muted-foreground">
No folders found. File will be created at Drive root.
</p>
)}
</div>
)}
</>
)}
</div>
@ -345,9 +354,11 @@ function ApprovalCard({
{/* Content preview */}
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3">
{(pendingEdits?.name ?? args.name) != null && (
<p className="text-sm font-medium text-foreground">{String(pendingEdits?.name ?? args.name)}</p>
)}
{(pendingEdits?.name ?? args.name) != null && (
<p className="text-sm font-medium text-foreground">
{String(pendingEdits?.name ?? args.name)}
</p>
)}
{(pendingEdits?.content ?? args.content) != null && (
<div
className="mt-2 max-h-[7rem] overflow-hidden text-sm"

View file

@ -1,14 +1,11 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import {
CornerDownLeftIcon,
InfoIcon,
} from "lucide-react";
import { CornerDownLeftIcon, InfoIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface GoogleDriveAccount {
@ -207,14 +204,12 @@ function ApprovalCard({
? "Google Drive File Deletion Approved"
: "Delete Google Drive File"}
</p>
{phase === "processing" ? (
{phase === "processing" ? (
<TextShimmerLoader text="Trashing file" size="sm" />
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">File trashed</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
File deletion was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">File deletion was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -274,22 +269,23 @@ function ApprovalCard({
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 space-y-3 select-none">
<p className="text-xs text-muted-foreground">
The file will be moved to Google Drive trash. You can restore it from trash within 30 days.
The file will be moved to Google Drive trash. You can restore it from trash within 30
days.
</p>
<div className="flex items-center gap-2.5">
<Checkbox
id="delete-from-kb"
checked={deleteFromKb}
onCheckedChange={(v) => setDeleteFromKb(v === true)}
className="shrink-0"
/>
<label htmlFor="delete-from-kb" className="flex-1 cursor-pointer">
<span className="text-sm text-foreground">Also remove from knowledge base</span>
<p className="text-xs text-muted-foreground mt-0.5">
This will permanently delete the file from your knowledge base (cannot be undone)
</p>
</label>
</div>
<div className="flex items-center gap-2.5">
<Checkbox
id="delete-from-kb"
checked={deleteFromKb}
onCheckedChange={(v) => setDeleteFromKb(v === true)}
className="shrink-0"
/>
<label htmlFor="delete-from-kb" className="flex-1 cursor-pointer">
<span className="text-sm text-foreground">Also remove from knowledge base</span>
<p className="text-xs text-muted-foreground mt-0.5">
This will permanently delete the file from your knowledge base (cannot be undone)
</p>
</label>
</div>
</div>
</>
)}
@ -298,27 +294,23 @@ function ApprovalCard({
{phase === "pending" && (
<>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 flex items-center gap-2 select-none">
<Button
size="sm"
className="rounded-lg gap-1.5"
onClick={handleApprove}
>
Approve
<CornerDownLeftIcon className="size-3 opacity-60" />
</Button>
<Button
size="sm"
variant="ghost"
className="rounded-lg text-muted-foreground"
onClick={() => {
setRejected();
onDecision({ type: "reject", message: "User rejected the action." });
}}
>
Reject
</Button>
</div>
<div className="px-5 py-4 flex items-center gap-2 select-none">
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
Approve
<CornerDownLeftIcon className="size-3 opacity-60" />
</Button>
<Button
size="sm"
variant="ghost"
className="rounded-lg text-muted-foreground"
onClick={() => {
setRejected();
onDecision({ type: "reject", message: "User rejected the action." });
}}
>
Reject
</Button>
</div>
</>
)}
</div>

View file

@ -1,8 +1,12 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import {
Select,
@ -11,11 +15,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
import { useSetAtom } from "jotai";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
interface JiraAccount {
id: number;
@ -151,7 +151,9 @@ function ApprovalCard({
const { phase, setProcessing, setRejected } = useHitlPhase(interruptData);
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{ title: string; description: string } | null>(null);
const [pendingEdits, setPendingEdits] = useState<{ title: string; description: string } | null>(
null
);
const [selectedAccountId, setSelectedAccountId] = useState("");
const [selectedProjectKey, setSelectedProjectKey] = useState(args.project_key ?? "");
@ -177,14 +179,23 @@ function ApprovalCard({
(overrides?: { title?: string; description?: string }) => {
return {
summary: overrides?.title ?? pendingEdits?.title ?? args.summary,
description: overrides?.description ?? pendingEdits?.description ?? args.description ?? null,
description:
overrides?.description ?? pendingEdits?.description ?? args.description ?? null,
connector_id: selectedAccountId ? Number(selectedAccountId) : null,
project_key: selectedProjectKey || null,
issue_type: selectedIssueType === "__none__" ? null : selectedIssueType,
priority: selectedPriority === "__none__" ? null : selectedPriority,
};
},
[args.summary, args.description, selectedAccountId, selectedProjectKey, selectedIssueType, selectedPriority, pendingEdits]
[
args.summary,
args.description,
selectedAccountId,
selectedProjectKey,
selectedIssueType,
selectedPriority,
pendingEdits,
]
);
const handleApprove = useCallback(() => {
@ -200,7 +211,17 @@ function ApprovalCard({
args: buildFinalArgs(),
},
});
}, [phase, setProcessing, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, buildFinalArgs, pendingEdits]);
}, [
phase,
setProcessing,
isPanelOpen,
canApprove,
allowedDecisions,
onDecision,
interruptData,
buildFinalArgs,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -225,15 +246,16 @@ function ApprovalCard({
: "Create Jira Issue"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={pendingEdits ? "Creating issue with your changes" : "Creating issue"} size="sm" />
<TextShimmerLoader
text={pendingEdits ? "Creating issue with your changes" : "Creating issue"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits ? "Issue created with your changes" : "Issue created"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Issue creation was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Issue creation was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -248,8 +270,8 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
openHitlEditPanel({
title: pendingEdits?.title ?? (args.summary ?? ""),
content: pendingEdits?.description ?? (args.description ?? ""),
title: pendingEdits?.title ?? args.summary ?? "",
content: pendingEdits?.description ?? args.description ?? "",
toolName: "Jira Issue",
onSave: (newTitle, newDescription) => {
setIsPanelOpen(false);
@ -316,10 +338,7 @@ function ApprovalCard({
<p className="text-xs font-medium text-muted-foreground">
Project <span className="text-destructive">*</span>
</p>
<Select
value={selectedProjectKey}
onValueChange={setSelectedProjectKey}
>
<Select value={selectedProjectKey} onValueChange={setSelectedProjectKey}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a project" />
</SelectTrigger>
@ -336,32 +355,26 @@ function ApprovalCard({
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground">Issue Type</p>
<Select
value={selectedIssueType}
onValueChange={setSelectedIssueType}
>
<Select value={selectedIssueType} onValueChange={setSelectedIssueType}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Task" />
</SelectTrigger>
<SelectContent>
{issueTypes.length > 0
? issueTypes.map((t) => (
{issueTypes.length > 0 ? (
issueTypes.map((t) => (
<SelectItem key={t.id} value={t.name}>
{t.name}
</SelectItem>
))
: (
<SelectItem value="Task">Task</SelectItem>
)}
) : (
<SelectItem value="Task">Task</SelectItem>
)}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground">Priority</p>
<Select
value={selectedPriority}
onValueChange={setSelectedPriority}
>
<Select value={selectedPriority} onValueChange={setSelectedPriority}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Default" />
</SelectTrigger>
@ -388,7 +401,9 @@ function ApprovalCard({
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3">
{(pendingEdits?.title ?? args.summary) != null && (
<p className="text-sm font-medium text-foreground">{pendingEdits?.title ?? args.summary}</p>
<p className="text-sm font-medium text-foreground">
{pendingEdits?.title ?? args.summary}
</p>
)}
{(pendingEdits?.description ?? args.description) != null && (
<div
@ -450,9 +465,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
All Jira accounts expired
</p>
<p className="text-sm font-semibold text-destructive">All Jira accounts expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -3,9 +3,9 @@
import { makeAssistantToolUI } from "@assistant-ui/react";
import { CornerDownLeftIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface JiraAccount {
@ -204,9 +204,7 @@ function ApprovalCard({
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">Issue deleted</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Issue deletion was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Issue deletion was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -280,11 +278,7 @@ function ApprovalCard({
<>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 flex items-center gap-2 select-none">
<Button
size="sm"
className="rounded-lg gap-1.5"
onClick={handleApprove}
>
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
Approve
<CornerDownLeftIcon className="size-3 opacity-60" />
</Button>
@ -310,9 +304,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Jira authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Jira authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -356,9 +348,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
Issue not found
</p>
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Issue not found</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -4,6 +4,9 @@ import { makeAssistantToolUI } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@ -13,10 +16,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
interface JiraIssue {
issue_id: string;
@ -194,9 +194,12 @@ function ApprovalCard({
const canEdit = allowedDecisions.includes("edit");
const hasProposedChanges =
actionArgs.new_summary || args.new_summary ||
actionArgs.new_description || args.new_description ||
actionArgs.new_priority || args.new_priority;
actionArgs.new_summary ||
args.new_summary ||
actionArgs.new_description ||
args.new_description ||
actionArgs.new_priority ||
args.new_priority;
const buildFinalArgs = useCallback(() => {
return {
@ -222,7 +225,16 @@ function ApprovalCard({
args: buildFinalArgs(),
},
});
}, [phase, setProcessing, isPanelOpen, allowedDecisions, onDecision, interruptData, buildFinalArgs, hasPanelEdits]);
}, [
phase,
setProcessing,
isPanelOpen,
allowedDecisions,
onDecision,
interruptData,
buildFinalArgs,
hasPanelEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -247,15 +259,16 @@ function ApprovalCard({
: "Update Jira Issue"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={hasPanelEdits ? "Updating issue with your changes" : "Updating issue"} size="sm" />
<TextShimmerLoader
text={hasPanelEdits ? "Updating issue with your changes" : "Updating issue"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{hasPanelEdits ? "Issue updated with your changes" : "Issue updated"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Issue update was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Issue update was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -366,14 +379,20 @@ function ApprovalCard({
{/* Content preview — proposed changes */}
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3">
{(hasProposedChanges || hasPanelEdits) ? (
{hasProposedChanges || hasPanelEdits ? (
<>
{(hasPanelEdits ? editedArgs.summary : (actionArgs.new_summary ?? args.new_summary)) && (
{(hasPanelEdits
? editedArgs.summary
: (actionArgs.new_summary ?? args.new_summary)) && (
<p className="text-sm font-medium text-foreground">
{String(hasPanelEdits ? editedArgs.summary : (actionArgs.new_summary ?? args.new_summary))}
{String(
hasPanelEdits ? editedArgs.summary : (actionArgs.new_summary ?? args.new_summary)
)}
</p>
)}
{(hasPanelEdits ? editedArgs.description : (actionArgs.new_description ?? args.new_description)) && (
{(hasPanelEdits
? editedArgs.description
: (actionArgs.new_description ?? args.new_description)) && (
<div
className="max-h-[7rem] overflow-hidden text-sm"
style={{
@ -382,7 +401,11 @@ function ApprovalCard({
}}
>
<PlateEditor
markdown={String(hasPanelEdits ? editedArgs.description : (actionArgs.new_description ?? args.new_description))}
markdown={String(
hasPanelEdits
? editedArgs.description
: (actionArgs.new_description ?? args.new_description)
)}
readOnly
preset="readonly"
editorVariant="none"
@ -445,9 +468,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Jira authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Jira authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -491,9 +512,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
Issue not found
</p>
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Issue not found</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -1,8 +1,12 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@ -13,11 +17,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
import { useSetAtom } from "jotai";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
interface LinearLabel {
id: string;
@ -148,7 +148,9 @@ function ApprovalCard({
const { phase, setProcessing, setRejected } = useHitlPhase(interruptData);
const [isPanelOpen, setIsPanelOpen] = useState(false);
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const [pendingEdits, setPendingEdits] = useState<{ title: string; description: string } | null>(null);
const [pendingEdits, setPendingEdits] = useState<{ title: string; description: string } | null>(
null
);
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState("");
const [selectedTeamId, setSelectedTeamId] = useState("");
@ -178,18 +180,32 @@ function ApprovalCard({
const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"];
const canEdit = allowedDecisions.includes("edit");
const buildFinalArgs = useCallback((overrides?: { title?: string; description?: string }) => {
return {
title: overrides?.title ?? pendingEdits?.title ?? args.title,
description: overrides?.description ?? pendingEdits?.description ?? args.description ?? null,
connector_id: selectedWorkspaceId ? Number(selectedWorkspaceId) : null,
team_id: selectedTeamId || null,
state_id: selectedStateId === "__none__" ? null : selectedStateId,
assignee_id: selectedAssigneeId === "__none__" ? null : selectedAssigneeId,
priority: Number(selectedPriority),
label_ids: selectedLabelIds,
};
}, [args.title, args.description, selectedWorkspaceId, selectedTeamId, selectedStateId, selectedAssigneeId, selectedPriority, selectedLabelIds, pendingEdits]);
const buildFinalArgs = useCallback(
(overrides?: { title?: string; description?: string }) => {
return {
title: overrides?.title ?? pendingEdits?.title ?? args.title,
description:
overrides?.description ?? pendingEdits?.description ?? args.description ?? null,
connector_id: selectedWorkspaceId ? Number(selectedWorkspaceId) : null,
team_id: selectedTeamId || null,
state_id: selectedStateId === "__none__" ? null : selectedStateId,
assignee_id: selectedAssigneeId === "__none__" ? null : selectedAssigneeId,
priority: Number(selectedPriority),
label_ids: selectedLabelIds,
};
},
[
args.title,
args.description,
selectedWorkspaceId,
selectedTeamId,
selectedStateId,
selectedAssigneeId,
selectedPriority,
selectedLabelIds,
pendingEdits,
]
);
const handleApprove = useCallback(() => {
if (phase !== "pending") return;
@ -204,7 +220,17 @@ function ApprovalCard({
args: buildFinalArgs(),
},
});
}, [phase, setProcessing, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, buildFinalArgs, pendingEdits]);
}, [
phase,
setProcessing,
isPanelOpen,
canApprove,
allowedDecisions,
onDecision,
interruptData,
buildFinalArgs,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -229,15 +255,16 @@ function ApprovalCard({
: "Create Linear Issue"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={pendingEdits ? "Creating issue with your changes" : "Creating issue"} size="sm" />
<TextShimmerLoader
text={pendingEdits ? "Creating issue with your changes" : "Creating issue"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits ? "Issue created with your changes" : "Issue created"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Issue creation was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Issue creation was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -252,8 +279,8 @@ function ApprovalCard({
onClick={() => {
setIsPanelOpen(true);
openHitlEditPanel({
title: pendingEdits?.title ?? (args.title ?? ""),
content: pendingEdits?.description ?? (args.description ?? ""),
title: pendingEdits?.title ?? args.title ?? "",
content: pendingEdits?.description ?? args.description ?? "",
toolName: "Linear Issue",
onSave: (newTitle, newDescription) => {
setIsPanelOpen(false);
@ -269,7 +296,7 @@ function ApprovalCard({
)}
</div>
{/* Context section — real pickers in pending */}
{/* Context section — real pickers in pending */}
{phase === "pending" && (
<>
<div className="mx-5 h-px bg-border/50" />
@ -278,43 +305,43 @@ function ApprovalCard({
<p className="text-sm text-destructive">{interruptData.context.error}</p>
) : (
<>
{workspaces.length > 0 && (
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground">
Linear Account <span className="text-destructive">*</span>
</p>
<Select
value={selectedWorkspaceId}
onValueChange={(v) => {
setSelectedWorkspaceId(v);
setSelectedTeamId("");
setSelectedStateId("__none__");
setSelectedAssigneeId("__none__");
setSelectedPriority("0");
setSelectedLabelIds([]);
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an account" />
</SelectTrigger>
<SelectContent>
{validWorkspaces.map((w) => (
<SelectItem key={w.id} value={String(w.id)}>
{w.name}
</SelectItem>
))}
{expiredWorkspaces.map((w) => (
<div
key={w.id}
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
>
{w.name} (expired, retry after re-auth)
</div>
))}
</SelectContent>
</Select>
</div>
)}
{workspaces.length > 0 && (
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground">
Linear Account <span className="text-destructive">*</span>
</p>
<Select
value={selectedWorkspaceId}
onValueChange={(v) => {
setSelectedWorkspaceId(v);
setSelectedTeamId("");
setSelectedStateId("__none__");
setSelectedAssigneeId("__none__");
setSelectedPriority("0");
setSelectedLabelIds([]);
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an account" />
</SelectTrigger>
<SelectContent>
{validWorkspaces.map((w) => (
<SelectItem key={w.id} value={String(w.id)}>
{w.name}
</SelectItem>
))}
{expiredWorkspaces.map((w) => (
<div
key={w.id}
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
>
{w.name} (expired, retry after re-auth)
</div>
))}
</SelectContent>
</Select>
</div>
)}
{selectedWorkspace && (
<>
@ -366,7 +393,10 @@ function ApprovalCard({
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground">Assignee</p>
<Select value={selectedAssigneeId} onValueChange={setSelectedAssigneeId}>
<Select
value={selectedAssigneeId}
onValueChange={setSelectedAssigneeId}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Unassigned" />
</SelectTrigger>
@ -520,9 +550,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
All Linear accounts expired
</p>
<p className="text-sm font-semibold text-destructive">All Linear accounts expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -3,9 +3,9 @@
import { makeAssistantToolUI } from "@assistant-ui/react";
import { CornerDownLeftIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface InterruptResult {
@ -150,7 +150,15 @@ function ApprovalCard({
},
},
});
}, [phase, setProcessing, onDecision, interruptData, issue?.id, context?.workspace?.id, deleteFromKb]);
}, [
phase,
setProcessing,
onDecision,
interruptData,
issue?.id,
context?.workspace?.id,
deleteFromKb,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -174,14 +182,12 @@ function ApprovalCard({
? "Linear Issue Deletion Approved"
: "Delete Linear Issue"}
</p>
{phase === "processing" ? (
{phase === "processing" ? (
<TextShimmerLoader text="Deleting issue" size="sm" />
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">Issue deleted</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Issue deletion was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Issue deletion was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -232,20 +238,20 @@ function ApprovalCard({
<>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 select-none">
<div className="flex items-center gap-2.5">
<Checkbox
id="linear-delete-from-kb"
checked={deleteFromKb}
onCheckedChange={(v) => setDeleteFromKb(v === true)}
className="shrink-0"
/>
<label htmlFor="linear-delete-from-kb" className="flex-1 cursor-pointer">
<span className="text-sm text-foreground">Also remove from knowledge base</span>
<p className="text-xs text-muted-foreground mt-0.5">
This will permanently delete the issue from your knowledge base (cannot be undone)
</p>
</label>
</div>
<div className="flex items-center gap-2.5">
<Checkbox
id="linear-delete-from-kb"
checked={deleteFromKb}
onCheckedChange={(v) => setDeleteFromKb(v === true)}
className="shrink-0"
/>
<label htmlFor="linear-delete-from-kb" className="flex-1 cursor-pointer">
<span className="text-sm text-foreground">Also remove from knowledge base</span>
<p className="text-xs text-muted-foreground mt-0.5">
This will permanently delete the issue from your knowledge base (cannot be undone)
</p>
</label>
</div>
</div>
</>
)}
@ -254,12 +260,8 @@ function ApprovalCard({
{phase === "pending" && (
<>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 flex items-center gap-2 select-none">
<Button
size="sm"
className="rounded-lg gap-1.5"
onClick={handleApprove}
>
<div className="px-5 py-4 flex items-center gap-2 select-none">
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
Approve
<CornerDownLeftIcon className="size-3 opacity-60" />
</Button>
@ -285,9 +287,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Linear authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Linear authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -315,9 +315,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
Issue not found
</p>
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Issue not found</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -4,6 +4,9 @@ import { makeAssistantToolUI } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@ -14,10 +17,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
interface LinearLabel {
id: string;
@ -110,7 +110,12 @@ interface AuthErrorResult {
connector_type: string;
}
type UpdateLinearIssueResult = InterruptResult | SuccessResult | ErrorResult | NotFoundResult | AuthErrorResult;
type UpdateLinearIssueResult =
| InterruptResult
| SuccessResult
| ErrorResult
| NotFoundResult
| AuthErrorResult;
function isInterruptResult(result: unknown): result is InterruptResult {
return (
@ -178,7 +183,9 @@ function ApprovalCard({
const issue = context?.issue;
const initialEditState = {
title: actionArgs.new_title ? String(actionArgs.new_title) : (issue?.title ?? args.new_title ?? ""),
title: actionArgs.new_title
? String(actionArgs.new_title)
: (issue?.title ?? args.new_title ?? ""),
description: actionArgs.new_description
? String(actionArgs.new_description)
: (issue?.description ?? args.new_description ?? ""),
@ -256,8 +263,10 @@ function ApprovalCard({
);
const hasProposedChanges =
actionArgs.new_title || args.new_title ||
actionArgs.new_description || args.new_description ||
actionArgs.new_title ||
args.new_title ||
actionArgs.new_description ||
args.new_description ||
proposedStateName ||
proposedAssigneeName ||
proposedPriorityLabel ||
@ -276,7 +285,16 @@ function ApprovalCard({
args: buildFinalArgs(),
},
});
}, [phase, setProcessing, isPanelOpen, allowedDecisions, onDecision, interruptData, buildFinalArgs, hasPanelEdits]);
}, [
phase,
setProcessing,
isPanelOpen,
allowedDecisions,
onDecision,
interruptData,
buildFinalArgs,
hasPanelEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -301,15 +319,16 @@ function ApprovalCard({
: "Update Linear Issue"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={hasPanelEdits ? "Updating issue with your changes" : "Updating issue"} size="sm" />
<TextShimmerLoader
text={hasPanelEdits ? "Updating issue with your changes" : "Updating issue"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{hasPanelEdits ? "Issue updated with your changes" : "Issue updated"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Issue update was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Issue update was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -346,7 +365,7 @@ function ApprovalCard({
)}
</div>
{/* Context section — workspace + current issue + pickers in pending */}
{/* Context section — workspace + current issue + pickers in pending */}
{phase === "pending" && (
<>
<div className="mx-5 h-px bg-border/50" />
@ -385,7 +404,9 @@ function ApprovalCard({
)}
{issue.current_assignee && <span>{issue.current_assignee.name}</span>}
{priorities.find((p) => p.priority === issue.priority) && (
<span>{priorities.find((p) => p.priority === issue.priority)?.label}</span>
<span>
{priorities.find((p) => p.priority === issue.priority)?.label}
</span>
)}
</div>
{issue.current_labels && issue.current_labels.length > 0 && (
@ -510,9 +531,7 @@ function ApprovalCard({
? `${label.color}70`
: `${label.color}28`,
color: label.color,
borderColor: isSelected
? `${label.color}cc`
: "transparent",
borderColor: isSelected ? `${label.color}cc` : "transparent",
}}
>
<span
@ -538,12 +557,18 @@ function ApprovalCard({
{/* Content preview — proposed changes */}
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3">
{(hasProposedChanges || hasPanelEdits) ? (
{hasProposedChanges || hasPanelEdits ? (
<>
{(hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title)) && (
<p className="text-sm font-medium text-foreground">{String(hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title))}</p>
<p className="text-sm font-medium text-foreground">
{String(
hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title)
)}
</p>
)}
{(hasPanelEdits ? editedArgs.description : (actionArgs.new_description ?? args.new_description)) && (
{(hasPanelEdits
? editedArgs.description
: (actionArgs.new_description ?? args.new_description)) && (
<div
className="max-h-[7rem] overflow-hidden text-sm"
style={{
@ -552,7 +577,11 @@ function ApprovalCard({
}}
>
<PlateEditor
markdown={String(hasPanelEdits ? editedArgs.description : (actionArgs.new_description ?? args.new_description))}
markdown={String(
hasPanelEdits
? editedArgs.description
: (actionArgs.new_description ?? args.new_description)
)}
readOnly
preset="readonly"
editorVariant="none"
@ -641,9 +670,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Linear authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Linear authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -671,9 +698,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
Issue not found
</p>
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Issue not found</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -4,6 +4,9 @@ import { makeAssistantToolUI } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import {
Select,
@ -12,9 +15,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface InterruptResult {
@ -123,8 +123,8 @@ function ApprovalCard({
const [pendingEdits, setPendingEdits] = useState<{ title: string; content: string } | null>(null);
const accounts = interruptData.context?.accounts ?? [];
const validAccounts = accounts.filter(a => !a.auth_expired);
const expiredAccounts = accounts.filter(a => a.auth_expired);
const validAccounts = accounts.filter((a) => !a.auth_expired);
const expiredAccounts = accounts.filter((a) => a.auth_expired);
const parentPages = interruptData.context?.parent_pages ?? {};
const defaultAccountId = useMemo(() => {
@ -166,12 +166,23 @@ function ApprovalCard({
...args,
...(pendingEdits && { title: pendingEdits.title, content: pendingEdits.content }),
connector_id: selectedAccountId ? Number(selectedAccountId) : null,
parent_page_id:
selectedParentPageId === "__none__" ? null : selectedParentPageId,
parent_page_id: selectedParentPageId === "__none__" ? null : selectedParentPageId,
},
},
});
}, [phase, isPanelOpen, selectedAccountId, isTitleValid, allowedDecisions, setProcessing, onDecision, interruptData, args, selectedParentPageId, pendingEdits]);
}, [
phase,
isPanelOpen,
selectedAccountId,
isTitleValid,
allowedDecisions,
setProcessing,
onDecision,
interruptData,
args,
selectedParentPageId,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -184,9 +195,7 @@ function ApprovalCard({
}, [handleApprove]);
return (
<div
className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300"
>
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300">
{/* Header */}
<div className="flex items-start justify-between px-5 pt-5 pb-4 select-none">
<div>
@ -198,15 +207,16 @@ function ApprovalCard({
: "Create Notion Page"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={pendingEdits ? "Creating page with your changes" : "Creating page"} size="sm" />
<TextShimmerLoader
text={pendingEdits ? "Creating page with your changes" : "Creating page"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits ? "Page created with your changes" : "Page created"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Page creation was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Page creation was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -238,7 +248,7 @@ function ApprovalCard({
)}
</div>
{/* Account/workspace picker — real UI in pending */}
{/* Account/workspace picker — real UI in pending */}
{phase === "pending" && interruptData.context && (
<>
<div className="mx-5 h-px bg-border/50" />
@ -247,39 +257,39 @@ function ApprovalCard({
<p className="text-sm text-destructive">{interruptData.context.error}</p>
) : (
<>
{accounts.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Notion Account <span className="text-destructive">*</span>
</p>
<Select
value={selectedAccountId}
onValueChange={(value) => {
setSelectedAccountId(value);
setSelectedParentPageId("__none__");
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an account" />
</SelectTrigger>
<SelectContent>
{validAccounts.map((account) => (
<SelectItem key={account.id} value={String(account.id)}>
{account.workspace_name}
</SelectItem>
))}
{expiredAccounts.map((a) => (
<div
key={a.id}
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
>
{a.workspace_name} (expired, retry after re-auth)
</div>
))}
</SelectContent>
</Select>
</div>
)}
{accounts.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Notion Account <span className="text-destructive">*</span>
</p>
<Select
value={selectedAccountId}
onValueChange={(value) => {
setSelectedAccountId(value);
setSelectedParentPageId("__none__");
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an account" />
</SelectTrigger>
<SelectContent>
{validAccounts.map((account) => (
<SelectItem key={account.id} value={String(account.id)}>
{account.workspace_name}
</SelectItem>
))}
{expiredAccounts.map((a) => (
<div
key={a.id}
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
>
{a.workspace_name} (expired, retry after re-auth)
</div>
))}
</SelectContent>
</Select>
</div>
)}
{selectedAccountId && (
<div className="space-y-2">
@ -316,7 +326,9 @@ function ApprovalCard({
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-3">
{(pendingEdits?.title ?? args.title) != null && (
<p className="text-sm font-medium text-foreground">{String(pendingEdits?.title ?? args.title)}</p>
<p className="text-sm font-medium text-foreground">
{String(pendingEdits?.title ?? args.title)}
</p>
)}
{(pendingEdits?.content ?? args.content) != null && (
<div
@ -378,9 +390,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Notion authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Notion authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -3,9 +3,9 @@
import { makeAssistantToolUI } from "@assistant-ui/react";
import { CornerDownLeftIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface InterruptResult {
@ -183,14 +183,12 @@ function ApprovalCard({
? "Notion Page Deletion Approved"
: "Delete Notion Page"}
</p>
{phase === "processing" ? (
{phase === "processing" ? (
<TextShimmerLoader text="Deleting page" size="sm" />
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">Page deleted</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Page deletion was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Page deletion was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -259,11 +257,7 @@ function ApprovalCard({
<>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4 flex items-center gap-2 select-none">
<Button
size="sm"
className="rounded-lg gap-1.5"
onClick={handleApprove}
>
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
Approve
<CornerDownLeftIcon className="size-3 opacity-60" />
</Button>
@ -289,9 +283,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
All Notion accounts expired
</p>
<p className="text-sm font-semibold text-destructive">All Notion accounts expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -319,9 +311,7 @@ function InfoCard({ result }: { result: InfoResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
Page not found
</p>
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Page not found</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -4,10 +4,10 @@ import { makeAssistantToolUI } from "@assistant-ui/react";
import { useSetAtom } from "jotai";
import { CornerDownLeftIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { PlateEditor } from "@/components/editor/plate-editor";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
import { Button } from "@/components/ui/button";
import { useHitlPhase } from "@/hooks/use-hitl-phase";
interface InterruptResult {
@ -68,7 +68,12 @@ interface AuthErrorResult {
connector_type: string;
}
type UpdateNotionPageResult = InterruptResult | SuccessResult | ErrorResult | InfoResult | AuthErrorResult;
type UpdateNotionPageResult =
| InterruptResult
| SuccessResult
| ErrorResult
| InfoResult
| AuthErrorResult;
function isInterruptResult(result: unknown): result is InterruptResult {
return (
@ -148,7 +153,17 @@ function ApprovalCard({
},
},
});
}, [phase, isPanelOpen, allowedDecisions, setProcessing, onDecision, interruptData, args, account?.id, pendingEdits]);
}, [
phase,
isPanelOpen,
allowedDecisions,
setProcessing,
onDecision,
interruptData,
args,
account?.id,
pendingEdits,
]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -173,15 +188,16 @@ function ApprovalCard({
: "Update Notion Page"}
</p>
{phase === "processing" ? (
<TextShimmerLoader text={pendingEdits ? "Updating page with your changes" : "Updating page"} size="sm" />
<TextShimmerLoader
text={pendingEdits ? "Updating page with your changes" : "Updating page"}
size="sm"
/>
) : phase === "complete" ? (
<p className="text-xs text-muted-foreground mt-0.5">
{pendingEdits ? "Page updated with your changes" : "Page updated"}
</p>
) : phase === "rejected" ? (
<p className="text-xs text-muted-foreground mt-0.5">
Page update was cancelled
</p>
<p className="text-xs text-muted-foreground mt-0.5">Page update was cancelled</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Requires your approval to proceed
@ -213,7 +229,7 @@ function ApprovalCard({
)}
</div>
{/* Context section — real UI in pending/processing/complete */}
{/* Context section — real UI in pending/processing/complete */}
{phase !== "rejected" && interruptData.context && (
<>
<div className="mx-5 h-px bg-border/50" />
@ -310,9 +326,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-destructive">
Notion authentication expired
</p>
<p className="text-sm font-semibold text-destructive">Notion authentication expired</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">
@ -340,9 +354,7 @@ function InfoCard({ result }: { result: InfoResult }) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
Page not found
</p>
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Page not found</p>
</div>
<div className="mx-5 h-px bg-border/50" />
<div className="px-5 py-4">

View file

@ -15,7 +15,8 @@ const buttonVariants = cva(
outline:
"border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground focus-visible:ring-0 focus-visible:ring-offset-0",
ghost:
"hover:bg-accent hover:text-accent-foreground focus-visible:ring-0 focus-visible:ring-offset-0",
link: "text-primary underline-offset-4 hover:underline",
},
size: {

View file

@ -170,9 +170,7 @@ function HeroCarousel() {
const id = setTimeout(() => {
directionRef.current = "forward";
setActiveIndex((prev) =>
prev >= carouselItems.length - 1 ? 0 : prev + 1
);
setActiveIndex((prev) => (prev >= carouselItems.length - 1 ? 0 : prev + 1));
}, AUTOPLAY_MS);
return () => clearTimeout(id);

View file

@ -1,45 +1,45 @@
"use client"
"use client";
import type * as React from "react"
import { CircleIcon } from "lucide-react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { CircleIcon } from "lucide-react";
import type * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function RadioGroup({
className,
...props
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
)
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
);
}
function RadioGroupItem({
className,
...props
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"aspect-square size-4 shrink-0 rounded-full border border-input text-primary shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-primary" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"aspect-square size-4 shrink-0 rounded-full border border-input text-primary shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-primary" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
}
export { RadioGroup, RadioGroupItem }
export { RadioGroup, RadioGroupItem };

View file

@ -1,83 +1,81 @@
"use client"
"use client";
import * as React from "react"
import type { VariantProps } from "class-variance-authority"
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
import type { VariantProps } from "class-variance-authority";
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui";
import * as React from "react";
import { toggleVariants } from "@/components/ui/toggle";
import { cn } from "@/lib/utils";
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants> & {
spacing?: number
}
VariantProps<typeof toggleVariants> & {
spacing?: number;
}
>({
size: "default",
variant: "default",
spacing: 0,
})
size: "default",
variant: "default",
spacing: 0,
});
function ToggleGroup({
className,
variant,
size,
spacing = 0,
children,
...props
className,
variant,
size,
spacing = 0,
children,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants> & {
spacing?: number
}) {
return (
<ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant}
data-size={size}
data-spacing={spacing}
style={{ "--gap": spacing } as React.CSSProperties}
className={cn(
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
className
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
)
VariantProps<typeof toggleVariants> & {
spacing?: number;
}) {
return (
<ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant}
data-size={size}
data-spacing={spacing}
style={{ "--gap": spacing } as React.CSSProperties}
className={cn(
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
className
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
);
}
function ToggleGroupItem({
className,
children,
variant,
size,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext)
className,
children,
variant,
size,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
data-spacing={context.spacing}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
return (
<ToggleGroupPrimitive.Item
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
data-spacing={context.spacing}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
}
export { ToggleGroup, ToggleGroupItem }
export { ToggleGroup, ToggleGroupItem };

View file

@ -1,47 +1,46 @@
"use client"
"use client";
import type * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Toggle as TogglePrimitive } from "radix-ui"
import { cva, type VariantProps } from "class-variance-authority";
import { Toggle as TogglePrimitive } from "radix-ui";
import type * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 min-w-9 px-2",
sm: "h-8 min-w-8 px-1.5",
lg: "h-10 min-w-10 px-2.5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 min-w-9 px-2",
sm: "h-8 min-w-8 px-1.5",
lg: "h-10 min-w-10 px-2.5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
function Toggle({
className,
variant,
size,
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
)
className,
variant,
size,
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Toggle, toggleVariants }
export { Toggle, toggleVariants };