- Added a new task store in `src-react/stores/task.ts` to manage tasks and their statuses. - Implemented functions for creating, executing, and retrying tasks, along with handling task progress and completion. - Introduced persistence for tasks using IPC. - Created utility functions for normalizing room types and building subtasks. - Added a new CSS file for global styles in `src-react/styles.css`. - Created runtime types in `src-react/types/runtime.ts` and exported them. - Updated the main entry points for Vue and React applications to support dynamic framework loading. - Refactored chat model interfaces and utility functions into `src/shared/chat-model.ts`. - Updated TypeScript configuration to include paths for React components and types. - Enhanced Vite configuration to support both Vue and React frameworks.
126 lines
5.2 KiB
TypeScript
126 lines
5.2 KiB
TypeScript
import { useRef } from 'react';
|
||
import type { AttachedFileMeta } from '@shared/chat-model';
|
||
|
||
type ChatComposerProps = {
|
||
value: string;
|
||
isSending: boolean;
|
||
attachments: AttachedFileMeta[];
|
||
error?: string | null;
|
||
onChange: (value: string) => void;
|
||
onSend: () => void;
|
||
onStop: () => void;
|
||
onAttach: (files: File[]) => void | Promise<void>;
|
||
onRemoveAttachment: (index: number) => void;
|
||
onDismissError?: () => void;
|
||
};
|
||
|
||
export default function ChatComposer({
|
||
value,
|
||
isSending,
|
||
attachments,
|
||
error,
|
||
onChange,
|
||
onSend,
|
||
onStop,
|
||
onAttach,
|
||
onRemoveAttachment,
|
||
onDismissError,
|
||
}: ChatComposerProps) {
|
||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||
|
||
return (
|
||
<div className="border-t border-[#edf2f7] px-6 py-4 dark:border-[#2a2a2d]">
|
||
<div className="rounded-[18px] border border-[#dfeaf6] bg-white p-4 shadow-[0_10px_30px_rgba(15,23,42,0.04)] dark:border-[#2a2a2d] dark:bg-[#1f1f22]">
|
||
<div className="flex items-start gap-3">
|
||
<div className="mt-1 flex h-9 w-9 flex-none items-center justify-center rounded-full bg-[#eff6ff] text-xs font-bold text-[#2B7FFF] dark:bg-[#222225]">
|
||
AI
|
||
</div>
|
||
<div className="min-w-0 flex-1">
|
||
{error ? (
|
||
<div className="mb-3 flex items-center justify-between gap-3 rounded-[14px] border border-[#fecaca] bg-[#fff1f2] px-4 py-3 text-sm text-[#b91c1c] dark:border-[#7f1d1d] dark:bg-[#2d1618] dark:text-[#fca5a5]">
|
||
<span className="min-w-0 flex-1">{error}</span>
|
||
{onDismissError ? (
|
||
<button type="button" className="text-xs transition-colors hover:text-[#7f1d1d]" onClick={onDismissError}>
|
||
关闭
|
||
</button>
|
||
) : null}
|
||
</div>
|
||
) : null}
|
||
<textarea
|
||
className="min-h-[120px] w-full resize-none rounded-[14px] border border-[#BEDBFF] bg-[#f8fbff] px-4 py-3 text-sm text-[#171717] outline-none transition-colors placeholder:text-[#99A0AE] focus:border-[#2B7FFF] dark:border-[#2a2a2d] dark:bg-[#232327] dark:text-gray-100 dark:placeholder:text-gray-500"
|
||
placeholder="输入消息,按 Enter 发送,Shift + Enter 换行"
|
||
value={value}
|
||
onChange={(event) => onChange(event.target.value)}
|
||
onKeyDown={(event) => {
|
||
if (event.key === 'Enter' && !event.shiftKey) {
|
||
event.preventDefault();
|
||
if (isSending) {
|
||
onStop();
|
||
return;
|
||
}
|
||
onSend();
|
||
}
|
||
}}
|
||
/>
|
||
{attachments.length > 0 ? (
|
||
<div className="mt-3 flex flex-wrap gap-2">
|
||
{attachments.map((attachment, index) => (
|
||
<div
|
||
key={attachment.filePath || `${attachment.fileName}-${index}`}
|
||
className="flex items-center gap-2 rounded-full border border-[#E5E8EE] bg-white px-3 py-1.5 text-xs text-[#525866] dark:border-[#2a2a2d] dark:bg-[#232327] dark:text-gray-300"
|
||
>
|
||
<span className="max-w-[180px] truncate">{attachment.fileName}</span>
|
||
<button
|
||
type="button"
|
||
className="text-[#99A0AE] transition-colors hover:text-[#ef4444]"
|
||
onClick={() => onRemoveAttachment(index)}
|
||
>
|
||
x
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : null}
|
||
<div className="mt-3 flex flex-wrap gap-2">
|
||
<input
|
||
ref={fileInputRef}
|
||
hidden
|
||
multiple
|
||
type="file"
|
||
onChange={(event) => {
|
||
const files = Array.from(event.target.files || []);
|
||
if (files.length > 0) {
|
||
void onAttach(files);
|
||
}
|
||
event.currentTarget.value = '';
|
||
}}
|
||
/>
|
||
<button
|
||
type="button"
|
||
className="rounded-full border border-[#E5E8EE] px-3 py-1.5 text-xs text-[#525866] transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:text-gray-300"
|
||
onClick={() => fileInputRef.current?.click()}
|
||
>
|
||
添加附件
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="rounded-full border border-[#E5E8EE] px-3 py-1.5 text-xs text-[#525866] transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:text-gray-300"
|
||
onClick={isSending ? onStop : onSend}
|
||
>
|
||
{isSending ? '停止生成' : '发送消息'}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="rounded-full border border-[#E5E8EE] px-3 py-1.5 text-xs text-[#525866] transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:text-gray-300"
|
||
onClick={() => onChange('')}
|
||
>
|
||
清空输入
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|