mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
e8e4b2e822
feat: support workspace (full) — store→business-hook + workspace router
231 lines
7.9 KiB
TypeScript
231 lines
7.9 KiB
TypeScript
'use client';
|
|
|
|
import { useEditor } from '@lobehub/editor/react';
|
|
import { ActionIcon, Block, Flexbox, Icon, Text } from '@lobehub/ui';
|
|
import { useModalContext } from '@lobehub/ui/base-ui';
|
|
import { Button } from 'antd';
|
|
import { cssVar } from 'antd-style';
|
|
import { Minimize2, Paperclip, UserCircle2, X } from 'lucide-react';
|
|
import { type KeyboardEvent, memo, useCallback, useEffect, useRef, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import { EditorCanvas } from '@/features/EditorCanvas';
|
|
import {
|
|
getAttachmentFileIdsFromEditor,
|
|
pickAndInsertAttachments,
|
|
} from '@/features/EditorCanvas/editorAttachments';
|
|
import { usePermission } from '@/hooks/usePermission';
|
|
import { useGlobalStore } from '@/store/global';
|
|
import { useTaskStore } from '@/store/task';
|
|
|
|
import AssigneeAgentSelector from '../features/AssigneeAgentSelector';
|
|
import AssigneeAvatar from '../features/AssigneeAvatar';
|
|
import TaskPriorityTag from '../features/TaskPriorityTag';
|
|
import { useAgentDisplayMeta } from '../shared/useAgentDisplayMeta';
|
|
|
|
export interface CreateTaskContentProps {
|
|
agentId?: string;
|
|
onCreated?: (task: { agentId?: string; identifier: string }) => void;
|
|
/**
|
|
* Whether to show the "minimize to inline entry" button. Only the list view has an
|
|
* inline entry target, so contexts like the Kanban board pass `false` to hide it.
|
|
*/
|
|
showInlineToggle?: boolean;
|
|
}
|
|
|
|
const CreateTaskContent = memo<CreateTaskContentProps>(
|
|
({ agentId, onCreated, showInlineToggle = true }) => {
|
|
const { t } = useTranslation('chat');
|
|
const { close } = useModalContext();
|
|
const { allowed: canCreateTask, reason } = usePermission('create_content');
|
|
|
|
const createTask = useTaskStore((s) => s.createTask);
|
|
const isCreating = useTaskStore((s) => s.isCreatingTask);
|
|
const updateSystemStatus = useGlobalStore((s) => s.updateSystemStatus);
|
|
|
|
const [title, setTitle] = useState('');
|
|
const [priority, setPriority] = useState(0);
|
|
const [assigneeAgentId, setAssigneeAgentId] = useState<string | undefined>(agentId);
|
|
|
|
const editor = useEditor();
|
|
const instructionRef = useRef('');
|
|
|
|
const assigneeMeta = useAgentDisplayMeta(assigneeAgentId);
|
|
|
|
const handleInline = useCallback(() => {
|
|
updateSystemStatus({ taskCreateInlineCollapsed: false }, 'expandTaskCreateInline');
|
|
close();
|
|
}, [close, updateSystemStatus]);
|
|
|
|
const handleContentChange = useCallback(() => {
|
|
if (!canCreateTask) return;
|
|
if (!editor) return;
|
|
instructionRef.current = String(editor.getDocument('markdown') ?? '');
|
|
}, [canCreateTask, editor]);
|
|
|
|
const handleAttach = useCallback(() => {
|
|
pickAndInsertAttachments(editor);
|
|
}, [editor]);
|
|
|
|
const handleSubmit = useCallback(async () => {
|
|
if (!canCreateTask) return;
|
|
const instruction = instructionRef.current.trim();
|
|
const hasFiles = getAttachmentFileIdsFromEditor(editor).length > 0;
|
|
if (!instruction && !title.trim() && !hasFiles) return;
|
|
|
|
const editorJson = editor?.getDocument?.('json') as unknown;
|
|
|
|
const result = await createTask({
|
|
assigneeAgentId,
|
|
editorData: editorJson,
|
|
instruction: instruction || title.trim(),
|
|
name: title.trim() || undefined,
|
|
priority: priority || undefined,
|
|
});
|
|
|
|
if (result) {
|
|
close();
|
|
onCreated?.({
|
|
agentId: result.assigneeAgentId ?? undefined,
|
|
identifier: result.identifier,
|
|
});
|
|
}
|
|
}, [assigneeAgentId, canCreateTask, close, createTask, editor, onCreated, priority, title]);
|
|
|
|
const handleSubmitRef = useRef(handleSubmit);
|
|
useEffect(() => {
|
|
handleSubmitRef.current = handleSubmit;
|
|
}, [handleSubmit]);
|
|
|
|
const handleKeyDown = useCallback((e: KeyboardEvent<HTMLDivElement>) => {
|
|
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
void handleSubmitRef.current?.();
|
|
}
|
|
}, []);
|
|
|
|
return (
|
|
<Flexbox onKeyDown={handleKeyDown}>
|
|
<Flexbox horizontal style={{ padding: '16px 24px 0' }}>
|
|
<Flexbox flex={1} style={{ minHeight: 180 }}>
|
|
<input
|
|
autoFocus={canCreateTask}
|
|
disabled={!canCreateTask}
|
|
placeholder={t('createTask.titlePlaceholder')}
|
|
value={title}
|
|
style={{
|
|
background: 'transparent',
|
|
border: 'none',
|
|
color: 'inherit',
|
|
fontFamily: 'inherit',
|
|
fontSize: 20,
|
|
fontWeight: 600,
|
|
lineHeight: 1.4,
|
|
outline: 'none',
|
|
padding: '4px 0',
|
|
width: '100%',
|
|
}}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
/>
|
|
<EditorCanvas
|
|
disabled={!canCreateTask}
|
|
editor={editor}
|
|
floatingToolbar={false}
|
|
placeholder={t('createTask.instructionPlaceholder')}
|
|
style={{ fontSize: 14, paddingBottom: 16 }}
|
|
onContentChange={handleContentChange}
|
|
/>
|
|
</Flexbox>
|
|
<Flexbox horizontal gap={4} style={{ flexShrink: 0 }}>
|
|
{showInlineToggle && (
|
|
<ActionIcon
|
|
icon={Minimize2}
|
|
title={t('createTask.expandToInline')}
|
|
onClick={handleInline}
|
|
/>
|
|
)}
|
|
<ActionIcon icon={X} onClick={close} />
|
|
</Flexbox>
|
|
</Flexbox>
|
|
|
|
<Flexbox
|
|
horizontal
|
|
align={'center'}
|
|
justify={'space-between'}
|
|
style={{ borderTop: `1px solid ${cssVar.colorBorderSecondary}`, padding: '8px 16px' }}
|
|
>
|
|
<Flexbox horizontal gap={2} wrap={'wrap'}>
|
|
<TaskPriorityTag priority={priority} onChange={setPriority}>
|
|
<Block
|
|
clickable
|
|
horizontal
|
|
align="center"
|
|
gap={6}
|
|
paddingBlock={4}
|
|
paddingInline={8}
|
|
variant={'borderless'}
|
|
>
|
|
<TaskPriorityTag disableDropdown priority={priority} size={14} />
|
|
<Text fontSize={12}>
|
|
{priority === 0
|
|
? t('taskDetail.priority.none')
|
|
: t(
|
|
`taskDetail.priority.${(['', 'urgent', 'high', 'normal', 'low'] as const)[priority]}` as never,
|
|
)}
|
|
</Text>
|
|
</Block>
|
|
</TaskPriorityTag>
|
|
|
|
<AssigneeAgentSelector currentAgentId={assigneeAgentId} onChange={setAssigneeAgentId}>
|
|
<Block
|
|
clickable
|
|
horizontal
|
|
align="center"
|
|
gap={6}
|
|
paddingBlock={4}
|
|
paddingInline={8}
|
|
variant={'borderless'}
|
|
>
|
|
{assigneeAgentId ? (
|
|
<>
|
|
<AssigneeAvatar agentId={assigneeAgentId} size={18} />
|
|
<Text fontSize={12}>{assigneeMeta?.title}</Text>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Icon color={cssVar.colorTextDescription} icon={UserCircle2} size={14} />
|
|
<Text color={cssVar.colorTextDescription} fontSize={12}>
|
|
{t('createTask.assignee')}
|
|
</Text>
|
|
</>
|
|
)}
|
|
</Block>
|
|
</AssigneeAgentSelector>
|
|
|
|
<ActionIcon
|
|
icon={Paperclip}
|
|
title={t('upload.action.tooltip')}
|
|
onClick={handleAttach}
|
|
/>
|
|
</Flexbox>
|
|
|
|
<Button
|
|
disabled={!canCreateTask || isCreating}
|
|
loading={isCreating}
|
|
shape={'round'}
|
|
size={'small'}
|
|
title={canCreateTask ? undefined : reason}
|
|
type={'primary'}
|
|
onClick={handleSubmit}
|
|
>
|
|
{t('createTask.submit')}
|
|
</Button>
|
|
</Flexbox>
|
|
</Flexbox>
|
|
);
|
|
},
|
|
);
|
|
|
|
export default CreateTaskContent;
|