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.
This commit is contained in:
125
src-react/components/chat/ChatComposer.tsx
Normal file
125
src-react/components/chat/ChatComposer.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
108
src-react/components/chat/ChatHistoryPanel.tsx
Normal file
108
src-react/components/chat/ChatHistoryPanel.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import type { ChatHistoryBucket } from './types';
|
||||
|
||||
type ChatHistoryPanelProps = {
|
||||
buckets: ChatHistoryBucket[];
|
||||
selectedConversationId?: string;
|
||||
loading?: boolean;
|
||||
onNewChat?: () => void;
|
||||
onSelectConversation?: (conversationId: string) => void;
|
||||
onRenameConversation?: (conversationId: string) => void;
|
||||
onDeleteConversation?: (conversationId: string) => void;
|
||||
};
|
||||
|
||||
export default function ChatHistoryPanel({
|
||||
buckets,
|
||||
selectedConversationId,
|
||||
loading,
|
||||
onNewChat,
|
||||
onSelectConversation,
|
||||
onRenameConversation,
|
||||
onDeleteConversation,
|
||||
}: ChatHistoryPanelProps) {
|
||||
const hasSessions = buckets.some((bucket) => bucket.sessions.length > 0);
|
||||
|
||||
return (
|
||||
<aside className="flex h-full min-h-0 w-full flex-none flex-col transition-all duration-300 md:w-[220px] lg:w-[230px]">
|
||||
<div className="flex h-full min-h-0 flex-col rounded-[20px] bg-white p-2 shadow-[0_10px_30px_rgba(15,23,42,0.06)] dark:bg-[#1b1b1d]">
|
||||
<div className="flex items-center gap-3 px-2 py-1.5">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-[#eff6ff] text-sm font-bold text-[#2B7FFF] dark:bg-[#222225]">
|
||||
YN
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-semibold text-[#171717] dark:text-gray-100">YINIAN</div>
|
||||
<div className="truncate text-xs text-[#99A0AE] dark:text-gray-500">对话历史</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="mt-2 flex items-center justify-center gap-2 rounded-lg border border-[#E5E8EE] bg-white px-3 py-2.5 text-sm text-[#171717] shadow-sm transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-100 dark:hover:border-[#2B7FFF]"
|
||||
onClick={onNewChat}
|
||||
>
|
||||
<span className="text-lg leading-none">+</span>
|
||||
<span>新对话</span>
|
||||
</button>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-1.5 py-3">
|
||||
{loading ? (
|
||||
<div className="rounded-lg border border-dashed border-[#dfeaf6] bg-[#f8fbff] px-4 py-6 text-sm text-[#99A0AE] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-400">
|
||||
正在加载会话历史...
|
||||
</div>
|
||||
) : null}
|
||||
{!loading && !hasSessions ? (
|
||||
<div className="rounded-lg border border-dashed border-[#dfeaf6] bg-[#f8fbff] px-4 py-6 text-sm text-[#99A0AE] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-400">
|
||||
还没有对话,点击上方“新对话”开始。
|
||||
</div>
|
||||
) : null}
|
||||
{buckets.map((bucket) => (
|
||||
<div key={bucket.key} className="mb-3 last:mb-0">
|
||||
<div className="px-2 pb-1 text-[11px] font-medium tracking-tight text-gray-400">{bucket.label}</div>
|
||||
<ul className="list-none space-y-2">
|
||||
{bucket.sessions.map((session) => {
|
||||
const isActive = session.conversationId === selectedConversationId;
|
||||
|
||||
return (
|
||||
<li key={session.conversationId}>
|
||||
<button
|
||||
type="button"
|
||||
className={[
|
||||
'flex w-full items-center gap-2 rounded-lg px-2 py-2 text-left text-sm transition-colors',
|
||||
isActive
|
||||
? 'border border-[#E5E8EE] bg-white text-[#171717] shadow-sm dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-100'
|
||||
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700/50',
|
||||
].join(' ')}
|
||||
onClick={() => onSelectConversation?.(session.conversationId)}
|
||||
>
|
||||
<span className="h-2 w-2 flex-none rounded-full bg-[#BEDBFF]" />
|
||||
<span className="min-w-0 flex-1 truncate">{session.title}</span>
|
||||
<span className="shrink-0 text-[11px] text-[#99A0AE] dark:text-gray-500">{session.updatedAt}</span>
|
||||
</button>
|
||||
{isActive ? (
|
||||
<div className="mt-1 flex items-center justify-end gap-2 pr-2 text-[11px]">
|
||||
<button
|
||||
type="button"
|
||||
className="text-[#99A0AE] transition-colors hover:text-[#2B7FFF] dark:text-gray-500"
|
||||
onClick={() => onRenameConversation?.(session.conversationId)}
|
||||
>
|
||||
重命名
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="text-[#99A0AE] transition-colors hover:text-[#ef4444] dark:text-gray-500"
|
||||
onClick={() => onDeleteConversation?.(session.conversationId)}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
92
src-react/components/chat/ChatMessageList.tsx
Normal file
92
src-react/components/chat/ChatMessageList.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import type { ChatMessageItem } from './types';
|
||||
|
||||
type ChatMessageListProps = {
|
||||
messages: ChatMessageItem[];
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
export default function ChatMessageList({ messages, loading }: ChatMessageListProps) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden px-6 py-6">
|
||||
<div ref={containerRef} className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto pr-1">
|
||||
{loading ? (
|
||||
<div className="rounded-[18px] border border-dashed border-[#BEDBFF] bg-[#EFF6FF] px-4 py-3 text-sm text-[#525866] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-400">
|
||||
正在加载会话内容...
|
||||
</div>
|
||||
) : null}
|
||||
{!loading && messages.length === 0 ? (
|
||||
<div className="rounded-[18px] border border-dashed border-[#BEDBFF] bg-[#EFF6FF] px-4 py-6 text-sm leading-7 text-[#525866] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-400">
|
||||
输入你的问题开始一段新对话,现有会话和流式响应都会直接显示在这里。
|
||||
</div>
|
||||
) : null}
|
||||
{messages.map((message) => (
|
||||
<article
|
||||
key={message.id}
|
||||
className={[
|
||||
'flex gap-3 rounded-[18px] border p-4',
|
||||
message.role === 'assistant'
|
||||
? 'border-[#E5E8EE] bg-[#f8fbff] dark:border-[#2a2a2d] dark:bg-[#1f1f22]'
|
||||
: 'border-[#dfeaf6] bg-white dark:border-[#2a2a2d] dark:bg-[#232327]',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex h-10 w-10 flex-none items-center justify-center rounded-full bg-[#eff6ff] text-sm font-bold text-[#2B7FFF] dark:bg-[#222225]">
|
||||
{message.role === 'assistant' ? 'AI' : 'ME'}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-semibold text-[#171717] dark:text-gray-100">{message.name}</div>
|
||||
<div className="text-xs text-[#99A0AE] dark:text-gray-500">{message.time}</div>
|
||||
</div>
|
||||
<p className="mt-2 whitespace-pre-wrap text-sm leading-7 text-[#525866] dark:text-gray-300">{message.content}</p>
|
||||
{message.attachments && message.attachments.length > 0 ? (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{message.attachments.map((attachment, index) => {
|
||||
const attachmentKey = attachment.filePath || `${attachment.fileName}-${index}`;
|
||||
const isImage = attachment.mimeType.startsWith('image/') && Boolean(attachment.preview);
|
||||
|
||||
if (isImage && attachment.preview) {
|
||||
return (
|
||||
<div
|
||||
key={attachmentKey}
|
||||
className="overflow-hidden rounded-xl border border-[#E5E8EE] bg-white dark:border-[#2a2a2d] dark:bg-[#232327]"
|
||||
>
|
||||
<img
|
||||
alt={attachment.fileName}
|
||||
className="h-24 w-24 object-cover"
|
||||
src={attachment.preview}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={attachmentKey}
|
||||
className="rounded-full border border-[#E5E8EE] px-3 py-1.5 text-xs text-[#525866] dark:border-[#2a2a2d] dark:text-gray-300"
|
||||
>
|
||||
{attachment.fileName}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{message.isStreaming ? (
|
||||
<div className="mt-3 text-xs text-[#2B7FFF]">正在生成回复...</div>
|
||||
) : null}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
114
src-react/components/chat/TaskBoard.tsx
Normal file
114
src-react/components/chat/TaskBoard.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import type { TaskItem } from './types';
|
||||
|
||||
type TaskBoardProps = {
|
||||
activeTab: 'pending' | 'completed';
|
||||
pendingItems: TaskItem[];
|
||||
completedItems: TaskItem[];
|
||||
onTabChange: (tab: 'pending' | 'completed') => void;
|
||||
onRemoveTask?: (taskId: string) => void;
|
||||
onRetryTask?: (taskId: string) => void;
|
||||
currentDateLabel?: string;
|
||||
currentTime?: string;
|
||||
};
|
||||
|
||||
export default function TaskBoard({
|
||||
activeTab,
|
||||
pendingItems,
|
||||
completedItems,
|
||||
onTabChange,
|
||||
onRemoveTask,
|
||||
onRetryTask,
|
||||
currentDateLabel,
|
||||
currentTime,
|
||||
}: TaskBoardProps) {
|
||||
const items = activeTab === 'pending' ? pendingItems : completedItems;
|
||||
|
||||
return (
|
||||
<aside className="h-full min-h-0 w-full flex-none lg:w-[288px]">
|
||||
<div className="flex h-full min-h-0 flex-col rounded-[20px] bg-white p-3 shadow-[0_10px_30px_rgba(15,23,42,0.06)] dark:bg-[#1b1b1d]">
|
||||
<div className="flex rounded-[10px] border border-[#BEDBFF] bg-[#EFF6FF] p-1 dark:border-[#2a2a2d] dark:bg-[#1f1f22]">
|
||||
<button
|
||||
type="button"
|
||||
className={[
|
||||
'flex-1 rounded-[8px] px-3 py-2 text-sm transition-colors',
|
||||
activeTab === 'pending'
|
||||
? 'bg-white text-[#2B7FFF] shadow-sm dark:bg-[#1f1f22]'
|
||||
: 'text-[#525866] dark:text-gray-300',
|
||||
].join(' ')}
|
||||
onClick={() => onTabChange('pending')}
|
||||
>
|
||||
待处理{pendingItems.length > 0 ? `(${pendingItems.length})` : ''}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={[
|
||||
'flex-1 rounded-[8px] px-3 py-2 text-sm transition-colors',
|
||||
activeTab === 'completed'
|
||||
? 'bg-white text-[#2B7FFF] shadow-sm dark:bg-[#1f1f22]'
|
||||
: 'text-[#525866] dark:text-gray-300',
|
||||
].join(' ')}
|
||||
onClick={() => onTabChange('completed')}
|
||||
>
|
||||
已处理{completedItems.length > 0 ? `(${completedItems.length})` : ''}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(currentDateLabel || currentTime) ? (
|
||||
<div className="flex items-center justify-between px-1 pb-3 pt-3 text-[13px] text-[#99A0AE] dark:text-gray-500">
|
||||
<span>{currentDateLabel || '执行时段'}</span>
|
||||
<span>{currentTime || '--'}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 overflow-y-auto">
|
||||
{items.length > 0 ? (
|
||||
items.map((item) => (
|
||||
<article
|
||||
key={item.id}
|
||||
className="relative flex gap-3 rounded-[12px] border border-[#dfeaf6] bg-white p-3.5 transition-colors hover:bg-[#F5F7FA] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:hover:bg-[#2a2a2d]"
|
||||
>
|
||||
{(onRemoveTask || onRetryTask) ? (
|
||||
<div className="absolute right-2 top-2 flex items-center gap-2">
|
||||
{onRetryTask && (item.status === 'failed' || item.status === 'partial_failed') ? (
|
||||
<button
|
||||
type="button"
|
||||
className="text-[11px] text-[#2B7FFF] transition-colors hover:text-[#1d4ed8]"
|
||||
onClick={() => onRetryTask(item.id)}
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
) : null}
|
||||
{onRemoveTask ? (
|
||||
<button
|
||||
type="button"
|
||||
className="text-[11px] text-[#99A0AE] transition-colors hover:text-[#ef4444]"
|
||||
onClick={() => onRemoveTask(item.removeTaskId || item.id)}
|
||||
>
|
||||
移除
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-lg border border-dashed border-[#9fc0e8] bg-[#EFF6FF] text-[23px] text-[#3b82f6] dark:border-gray-700 dark:bg-[#222225]">
|
||||
•
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-semibold text-[#171717] dark:text-gray-100">{item.title}</div>
|
||||
<div className="mt-1.5 text-[13px] leading-6 text-[#9aa5b1] dark:text-gray-400">{item.description}</div>
|
||||
{item.meta ? (
|
||||
<div className="mt-1 text-[11px] text-[#99A0AE] dark:text-gray-500">{item.meta}</div>
|
||||
) : null}
|
||||
<div className="mt-2 text-xs font-medium text-[#2B7FFF]">{item.status}</div>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<div className="rounded-[12px] border border-dashed border-[#dfeaf6] bg-[#f8fbff] px-4 py-6 text-sm text-[#99A0AE] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-400">
|
||||
当前分类下暂无任务卡片。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
5
src-react/components/chat/index.ts
Normal file
5
src-react/components/chat/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { default as ChatComposer } from './ChatComposer';
|
||||
export { default as ChatHistoryPanel } from './ChatHistoryPanel';
|
||||
export { default as ChatMessageList } from './ChatMessageList';
|
||||
export { default as TaskBoard } from './TaskBoard';
|
||||
export type { ChatHistoryBucket, ChatMessageItem, TaskItem, TaskTabValue } from './types';
|
||||
33
src-react/components/chat/types.ts
Normal file
33
src-react/components/chat/types.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { AttachedFileMeta } from '@shared/chat-model';
|
||||
|
||||
export type TaskTabValue = 'pending' | 'completed';
|
||||
|
||||
export type ChatHistoryBucket = {
|
||||
key: string;
|
||||
label: string;
|
||||
sessions: Array<{
|
||||
conversationId: string;
|
||||
title: string;
|
||||
updatedAt: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ChatMessageItem = {
|
||||
id: string;
|
||||
role: 'assistant' | 'user';
|
||||
name: string;
|
||||
time: string;
|
||||
content: string;
|
||||
attachments?: AttachedFileMeta[];
|
||||
isStreaming?: boolean;
|
||||
isError?: boolean;
|
||||
};
|
||||
|
||||
export type TaskItem = {
|
||||
id: string;
|
||||
removeTaskId?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
status: string;
|
||||
meta?: string;
|
||||
};
|
||||
Reference in New Issue
Block a user