Files
twenty/packages/twenty-front/src/modules/ai/components/AiChatThreadListItem.tsx
T
Raphaël Bosi c596a5e342 Rename twenty-ui to twenty-ui-deprecated and twenty-new-ui to twenty-ui to prepare package release (#21315)
## Description

Promotes the next-gen UI library (formerly `twenty-new-ui`) to the name
**`twenty-ui`** (v0.1.0, publishable) and renames the old package to
**`twenty-ui-deprecated`**. Rewrites ~1,730 `twenty-ui` imports →
`twenty-ui-deprecated`, updates all configs/CI/Docker/deps, and migrates
twenty-front's `Toggle` to the new package (first consumer) as a
drop-in.

## Next steps
- Wire the `ui/v*` publish dispatch (`cd-deploy-tag.yaml` +
`.yarnrc.yml`), then tag `ui/v0.1.0` to publish.
- Continue migrating components from `twenty-ui-deprecated` →
`twenty-ui`.
2026-06-08 18:12:28 +02:00

167 lines
5.2 KiB
TypeScript

import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useContext } from 'react';
import { Key } from 'ts-key-enum';
import { IconArchive, IconSparkles } from 'twenty-ui-deprecated/display';
import {
ThemeContext,
themeCssVariables,
} from 'twenty-ui-deprecated/theme-constants';
import { AiChatThreadItemMenu } from '@/ai/components/AiChatThreadItemMenu';
import { AI_CHAT_THREAD_ACTIONS_SURFACE } from '@/ai/constants/AiChatThreadActionsSurface';
import { useAiChatThreadClick } from '@/ai/hooks/useAiChatThreadClick';
import { useAiChatThreadRename } from '@/ai/hooks/useAiChatThreadRename';
import { getAiChatThreadItemMenuDropdownId } from '@/ai/utils/getAiChatThreadItemMenuDropdownId';
import { TextInput } from '@/ui/input/components/TextInput';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { type AgentChatThread } from '~/generated-metadata/graphql';
const StyledThreadItem = styled.div`
align-items: center;
border-left: 3px solid transparent;
border-radius: ${themeCssVariables.border.radius.sm};
cursor: pointer;
display: flex;
gap: ${themeCssVariables.spacing[2]};
margin-bottom: ${themeCssVariables.spacing[1]};
padding: ${themeCssVariables.spacing[1]} 1px;
position: relative;
right: 3px;
transition: all 0.2s ease;
width: calc(100% + 1px);
&:hover {
background: ${themeCssVariables.background.transparent.light};
}
`;
const StyledThreadIcon = styled.div<{ $isArchived: boolean }>`
align-items: center;
background: ${({ $isArchived }) =>
$isArchived
? themeCssVariables.background.transparent.lighter
: themeCssVariables.background.transparent.blue};
border-radius: ${themeCssVariables.border.radius.sm};
color: ${({ $isArchived }) =>
$isArchived
? themeCssVariables.font.color.tertiary
: themeCssVariables.color.blue};
display: flex;
justify-content: center;
padding: ${themeCssVariables.spacing[1]};
`;
const StyledThreadContent = styled.div`
flex: 1;
min-width: 0;
`;
const StyledThreadTitle = styled.div`
color: ${themeCssVariables.font.color.secondary};
font-size: ${themeCssVariables.font.size.md};
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const StyledMenuTrigger = styled.div<{ $isDropdownOpen: boolean }>`
opacity: ${({ $isDropdownOpen }) => ($isDropdownOpen ? 1 : 0)};
pointer-events: ${({ $isDropdownOpen }) =>
$isDropdownOpen ? 'auto' : 'none'};
position: absolute;
right: ${themeCssVariables.spacing[1]};
top: 50%;
transform: translateY(-50%);
transition: opacity 150ms;
${StyledThreadItem}:hover & {
opacity: 1;
pointer-events: auto;
}
`;
type AiChatThreadListItemProps = {
thread: AgentChatThread;
};
export const AiChatThreadListItem = ({ thread }: AiChatThreadListItemProps) => {
const { theme } = useContext(ThemeContext);
const { t } = useLingui();
const { handleThreadClick } = useAiChatThreadClick();
const {
isRenaming,
draftTitle,
setDraftTitle,
startRename,
cancelRename,
commitRename,
} = useAiChatThreadRename(thread);
const isArchived = Boolean(thread.deletedAt);
const ThreadIcon = isArchived ? IconArchive : IconSparkles;
const displayTitle = thread.title ?? t`Untitled`;
const itemMenuDropdownId = getAiChatThreadItemMenuDropdownId(
thread.id,
AI_CHAT_THREAD_ACTIONS_SURFACE.SIDE_PANEL,
);
const isDropdownOpen = useAtomComponentStateValue(
isDropdownOpenComponentState,
itemMenuDropdownId,
);
return (
<StyledThreadItem
onClick={() => {
if (!isRenaming) {
handleThreadClick(thread);
}
}}
>
<StyledThreadIcon $isArchived={isArchived}>
<ThreadIcon size={theme.icon.size.md} color="currentColor" />
</StyledThreadIcon>
<StyledThreadContent>
{isRenaming ? (
<TextInput
value={draftTitle}
onChange={setDraftTitle}
onClick={(event) => event.stopPropagation()}
onFocus={(event) => event.target.select()}
onBlur={() => commitRename(draftTitle)}
onKeyDown={(event) => {
if (event.key === Key.Enter) {
event.preventDefault();
void commitRename(draftTitle);
} else if (event.key === Key.Escape) {
event.preventDefault();
cancelRename();
}
}}
sizeVariant="sm"
fullWidth
autoFocus
aria-label={t`Rename chat`}
/>
) : (
<StyledThreadTitle>{displayTitle}</StyledThreadTitle>
)}
</StyledThreadContent>
<StyledMenuTrigger
$isDropdownOpen={isDropdownOpen}
onClick={(event) => event.stopPropagation()}
>
<AiChatThreadItemMenu
threadId={thread.id}
threadTitle={displayTitle}
isArchived={isArchived}
surface={AI_CHAT_THREAD_ACTIONS_SURFACE.SIDE_PANEL}
onRenameRequested={startRename}
/>
</StyledMenuTrigger>
</StyledThreadItem>
);
};