Files
zn-ai/src-react/components/chat/ChatComposer.tsx
duanshuwen b1dea9a5c2 feat: implement task management store with IPC integration
- 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.
2026-04-17 07:09:56 +08:00

126 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}