feat: 添加MinIO文件系统支持并优化外部连接器功能
- 添加MinIO用户文件系统配置选项(BEAVER_MINIO_ROOT_USER等) - 更新外部连接器配置结构,包括BASE_URL和认证令牌设置 - 改进connector provider支持更多类型(official, feishu_bot等) - 实现Mistral模型推理模式支持reasoning_effort参数 - 增强外部连接器策略配置和运行时配置管理 - 添加connector bridge事件验证和安全保护机制 - 优化任务路由逻辑,区分simple_chat和new_task场景 - 更新初始技能工具提示配置,分离authoring admin功能
This commit is contained in:
@ -1,254 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Circle,
|
||||
FileJson,
|
||||
FileOutput,
|
||||
FileText,
|
||||
Image as ImageIcon,
|
||||
Link2,
|
||||
ListChecks,
|
||||
Loader2,
|
||||
PanelRightOpen,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { Activity, PanelRightOpen, X } from 'lucide-react';
|
||||
|
||||
import { TaskTimeline } from '@/components/task-detail';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { appStatusLabel } from '@/lib/i18n/common';
|
||||
import { pickAppText } from '@/lib/i18n/core';
|
||||
import { useAppI18n } from '@/lib/i18n/provider';
|
||||
import type {
|
||||
SessionProgressArtifactView,
|
||||
SessionProgressStepView,
|
||||
SessionProgressView,
|
||||
} from '@/lib/session-progress';
|
||||
import type { ProcessArtifact, ProcessRunStatus } from '@/types';
|
||||
|
||||
function formatShortTime(value: string, locale: 'zh-CN' | 'en-US') {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function statusTone(status: ProcessRunStatus) {
|
||||
if (status === 'done') return 'text-[#2F8D50] bg-[#E3F1E7] border-[#B8D9C2]';
|
||||
if (status === 'running') return 'text-[#2F6FCA] bg-[#E7EEF9] border-[#B8CBE8]';
|
||||
if (status === 'error') return 'text-[#8A3A2D] bg-[#F0E5E1] border-[#D9BDB4]';
|
||||
if (status === 'cancelled') return 'text-[#6A5E58] bg-[#ECE8E5] border-[#D8D2CE]';
|
||||
return 'text-[#6A5E58] bg-[#F0ECE9] border-[#D8D2CE]';
|
||||
}
|
||||
|
||||
function StepMarker({ step, index }: { step: SessionProgressStepView; index: number }) {
|
||||
if (step.status === 'done') {
|
||||
return (
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-[#2F8D50] text-white">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (step.status === 'running') {
|
||||
return (
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-[#2F6FCA] text-[11px] font-semibold text-white">
|
||||
{index + 1}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (step.status === 'error') {
|
||||
return (
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-[#8A3A2D] text-white">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-[#D8D2CE] text-[#6A5E58]">
|
||||
<Circle className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function artifactIcon(type: ProcessArtifact['artifact_type']) {
|
||||
if (type === 'json') return <FileJson className="h-4 w-4" />;
|
||||
if (type === 'image') return <ImageIcon className="h-4 w-4" />;
|
||||
if (type === 'link') return <Link2 className="h-4 w-4" />;
|
||||
if (type === 'markdown' || type === 'text') return <FileText className="h-4 w-4" />;
|
||||
return <FileOutput className="h-4 w-4" />;
|
||||
}
|
||||
|
||||
function ProgressHeader({ view }: { view: SessionProgressView }) {
|
||||
const { locale } = useAppI18n();
|
||||
const percent = view.progress.percent;
|
||||
|
||||
return (
|
||||
<section className="rounded-lg border border-[#ECE7E3] bg-white px-4 py-4 shadow-[0_8px_24px_rgba(0,0,0,0.04)]">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[#E3F1E7] text-[#2F8D50]">
|
||||
<ListChecks className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="line-clamp-2 text-sm font-semibold text-foreground">{view.title}</div>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span className={`rounded-full border px-2 py-0.5 text-[11px] font-medium ${statusTone(view.status)}`}>
|
||||
{appStatusLabel(view.status, locale)}
|
||||
</span>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{pickAppText(locale, '更新于', 'Updated')} {formatShortTime(view.updatedAt, locale)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="mb-2 flex items-center justify-between gap-3 text-xs text-muted-foreground">
|
||||
<span>{view.progress.label}</span>
|
||||
{percent !== null && <span className="font-medium text-foreground">{percent}%</span>}
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-[#ECE8E5]">
|
||||
<div
|
||||
className="h-full rounded-full bg-[#5DB56F] transition-all"
|
||||
style={{ width: `${percent ?? 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{view.summary && (
|
||||
<p className="mt-3 line-clamp-3 text-xs leading-5 text-muted-foreground">{view.summary}</p>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function StepList({ steps }: { steps: SessionProgressStepView[] }) {
|
||||
const { locale } = useAppI18n();
|
||||
|
||||
return (
|
||||
<section className="rounded-lg border border-[#ECE7E3] bg-white px-4 py-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-foreground">
|
||||
{pickAppText(locale, '运行步骤', 'Run Steps')}
|
||||
</h3>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{pickAppText(locale, `${steps.length} 步`, `${steps.length} steps`)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-0">
|
||||
{steps.map((step, index) => (
|
||||
<div key={step.runId} className="grid grid-cols-[24px_1fr] gap-3">
|
||||
<div className="flex flex-col items-center">
|
||||
<StepMarker step={step} index={index} />
|
||||
{index < steps.length - 1 && <span className="mt-2 h-full min-h-8 w-px bg-[#E6E1DE]" />}
|
||||
</div>
|
||||
<div className="pb-5">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="line-clamp-2 text-sm font-medium text-foreground">
|
||||
{index + 1}. {step.title}
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] text-muted-foreground">
|
||||
{step.actorName} · {formatShortTime(step.updatedAt, locale)}
|
||||
</div>
|
||||
</div>
|
||||
<span className={`shrink-0 rounded-full border px-2 py-0.5 text-[11px] ${statusTone(step.status)}`}>
|
||||
{appStatusLabel(step.status, locale)}
|
||||
</span>
|
||||
</div>
|
||||
{step.description && (
|
||||
<p className="mt-2 line-clamp-3 text-xs leading-5 text-muted-foreground">
|
||||
{step.description}
|
||||
</p>
|
||||
)}
|
||||
{step.status === 'running' && (
|
||||
<div className="mt-2 flex items-center gap-1.5 text-[11px] text-[#2F6FCA]">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
<span>{pickAppText(locale, '正在处理', 'In progress')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ArtifactRow({ artifact }: { artifact: SessionProgressArtifactView }) {
|
||||
return (
|
||||
<a
|
||||
href={artifact.url || undefined}
|
||||
target={artifact.url ? '_blank' : undefined}
|
||||
rel={artifact.url ? 'noreferrer' : undefined}
|
||||
className="block rounded-lg border border-[#ECE7E3] bg-[#FDFDFC] px-3 py-3 transition-colors hover:bg-[#F7F6F5]"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[#ECE8E5] text-[#5F5550]">
|
||||
{artifactIcon(artifact.type)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium text-foreground">{artifact.title}</div>
|
||||
<div className="mt-1 text-[11px] text-muted-foreground">
|
||||
{artifact.actorName} · {artifact.typeLabel}
|
||||
</div>
|
||||
<p className="mt-2 line-clamp-2 text-xs leading-5 text-muted-foreground">{artifact.preview}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function ArtifactSection({ view }: { view: SessionProgressView }) {
|
||||
const { locale } = useAppI18n();
|
||||
|
||||
return (
|
||||
<section className="rounded-lg border border-[#ECE7E3] bg-white px-4 py-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-foreground">
|
||||
{pickAppText(locale, '生成内容', 'Generated Content')}
|
||||
</h3>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{pickAppText(locale, `${view.artifacts.length} 个`, `${view.artifacts.length} items`)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{view.artifactTypeSummaries.length > 0 ? (
|
||||
<div className="mb-3 flex flex-wrap gap-2">
|
||||
{view.artifactTypeSummaries.map((item) => (
|
||||
<span
|
||||
key={item.type}
|
||||
className="inline-flex items-center gap-1.5 rounded-full border border-[#E6E1DE] bg-[#F7F6F5] px-2.5 py-1 text-xs text-[#4F4642]"
|
||||
>
|
||||
{artifactIcon(item.type)}
|
||||
<span>{item.label}</span>
|
||||
<span className="font-semibold">{item.count}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="mb-3 text-xs text-muted-foreground">
|
||||
{pickAppText(locale, '暂时还没有生成内容。', 'No generated content yet.')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{view.artifacts.map((artifact) => (
|
||||
<ArtifactRow key={artifact.artifactId} artifact={artifact} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
import type { TaskTimelineCard } from '@/types';
|
||||
|
||||
function ProgressPanel({
|
||||
view,
|
||||
cards,
|
||||
isLive,
|
||||
onClose,
|
||||
}: {
|
||||
view: SessionProgressView;
|
||||
cards: TaskTimelineCard[];
|
||||
isLive: boolean;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { locale } = useAppI18n();
|
||||
@ -260,11 +27,14 @@ function ProgressPanel({
|
||||
<h2 className="text-base font-semibold text-foreground">
|
||||
{pickAppText(locale, '当前会话的运行进度', 'Current Session Progress')}
|
||||
</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{pickAppText(locale, '任务列表会自动刷新', 'Task updates refresh automatically')}
|
||||
<p className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
{isLive ? <Activity className="h-3.5 w-3.5" /> : null}
|
||||
{isLive
|
||||
? pickAppText(locale, '任务时间线实时更新', 'Task timeline updates live')
|
||||
: pickAppText(locale, '与任务详情时间线一致', 'Matches the Task detail timeline')}
|
||||
</p>
|
||||
</div>
|
||||
{onClose && (
|
||||
{onClose ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
@ -273,28 +43,32 @@ function ProgressPanel({
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<ScrollArea className="min-h-0 flex-1 px-4 py-4">
|
||||
<div className="space-y-4 pb-6">
|
||||
<ProgressHeader view={view} />
|
||||
<StepList steps={view.steps} />
|
||||
<ArtifactSection view={view} />
|
||||
<div className="pb-6">
|
||||
<TaskTimeline cards={cards} isLive={isLive} showHeader={false} />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CurrentSessionProgressSidebar({ view }: { view: SessionProgressView }) {
|
||||
export function CurrentSessionProgressSidebar({
|
||||
cards,
|
||||
isLive,
|
||||
}: {
|
||||
cards: TaskTimelineCard[];
|
||||
isLive: boolean;
|
||||
}) {
|
||||
const { locale } = useAppI18n();
|
||||
const [mobileOpen, setMobileOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside className="hidden h-full w-[380px] shrink-0 border-l border-[#E6E1DE] xl:flex">
|
||||
<ProgressPanel view={view} />
|
||||
<ProgressPanel cards={cards} isLive={isLive} />
|
||||
</aside>
|
||||
|
||||
<button
|
||||
@ -306,7 +80,7 @@ export function CurrentSessionProgressSidebar({ view }: { view: SessionProgressV
|
||||
<PanelRightOpen className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
{mobileOpen && (
|
||||
{mobileOpen ? (
|
||||
<div className="fixed inset-0 z-50 xl:hidden">
|
||||
<button
|
||||
type="button"
|
||||
@ -315,10 +89,10 @@ export function CurrentSessionProgressSidebar({ view }: { view: SessionProgressV
|
||||
aria-label={pickAppText(locale, '关闭进度面板', 'Close progress panel')}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 w-[min(92vw,390px)] border-l border-[#E6E1DE] shadow-2xl">
|
||||
<ProgressPanel view={view} onClose={() => setMobileOpen(false)} />
|
||||
<ProgressPanel cards={cards} isLive={isLive} onClose={() => setMobileOpen(false)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -10,6 +10,14 @@ import { getTaskCardMessageIndexes, hasVisibleChatContent, normalizedMessageText
|
||||
import { AgentTeamBlock } from '@/components/chat-workbench/AgentTeamBlock';
|
||||
import { MarkdownContent } from '@/components/chat-workbench/MarkdownContent';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { pickAppText } from '@/lib/i18n/core';
|
||||
import { useAppI18n } from '@/lib/i18n/provider';
|
||||
import { containedPreservedLongTextClass } from '@/lib/text-wrapping';
|
||||
@ -58,6 +66,8 @@ function MessageBubble({
|
||||
const textContent = normalizedMessageText(message.content);
|
||||
const [feedbackMode, setFeedbackMode] = React.useState<'accept' | null>(null);
|
||||
const [feedbackComment, setFeedbackComment] = React.useState('');
|
||||
const [confirmAbandonOpen, setConfirmAbandonOpen] = React.useState(false);
|
||||
const feedbackTextareaId = message.run_id ? `feedback-note-${message.run_id}` : undefined;
|
||||
|
||||
return (
|
||||
<div className={`flex gap-3 ${isUser ? 'justify-end' : ''}`}>
|
||||
@ -130,7 +140,7 @@ function MessageBubble({
|
||||
</div>
|
||||
<Link
|
||||
href={`/tasks/${encodeURIComponent(message.task_id)}`}
|
||||
className="inline-flex h-8 items-center gap-1 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
||||
className="inline-flex h-11 items-center gap-1 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
{pickAppText(locale, '查看任务', 'Open task')}
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
@ -157,7 +167,7 @@ function MessageBubble({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFeedbackMode('accept')}
|
||||
className="inline-flex h-8 items-center gap-1 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
className="inline-flex h-11 items-center gap-1 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<ThumbsUp className="h-3.5 w-3.5" />
|
||||
{pickAppText(locale, '接受', 'Accept')}
|
||||
@ -165,15 +175,15 @@ function MessageBubble({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRequestRevision(message.run_id!)}
|
||||
className="inline-flex h-8 items-center gap-1 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
className="inline-flex h-11 items-center gap-1 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<RefreshCcw className="h-3.5 w-3.5" />
|
||||
{pickAppText(locale, '需要修改', 'Revise')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onFeedback(message.run_id!, 'abandon')}
|
||||
className="inline-flex h-8 items-center gap-1 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
onClick={() => setConfirmAbandonOpen(true)}
|
||||
className="inline-flex h-11 items-center gap-1 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<XCircle className="h-3.5 w-3.5" />
|
||||
{pickAppText(locale, '放弃', 'Abandon')}
|
||||
@ -181,7 +191,11 @@ function MessageBubble({
|
||||
</div>
|
||||
{feedbackMode && (
|
||||
<div className="space-y-2 rounded-md border border-border bg-background p-2">
|
||||
<label htmlFor={feedbackTextareaId} className="sr-only">
|
||||
{pickAppText(locale, '接受反馈备注', 'Acceptance note')}
|
||||
</label>
|
||||
<textarea
|
||||
id={feedbackTextareaId}
|
||||
value={feedbackComment}
|
||||
onChange={(event) => setFeedbackComment(event.target.value)}
|
||||
placeholder={pickAppText(locale, '可选:补充说明...', 'Optional note...')}
|
||||
@ -194,14 +208,14 @@ function MessageBubble({
|
||||
setFeedbackMode(null);
|
||||
setFeedbackComment('');
|
||||
}}
|
||||
className="h-8 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent"
|
||||
className="h-11 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent"
|
||||
>
|
||||
{pickAppText(locale, '取消', 'Cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onFeedback(message.run_id!, feedbackMode, feedbackComment.trim() || undefined)}
|
||||
className="h-8 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
||||
className="h-11 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
{pickAppText(locale, '提交', 'Submit')}
|
||||
</button>
|
||||
@ -210,6 +224,35 @@ function MessageBubble({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Dialog open={confirmAbandonOpen} onOpenChange={setConfirmAbandonOpen}>
|
||||
<DialogContent className="max-w-[calc(100vw-2rem)] sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{pickAppText(locale, '放弃当前任务?', 'Abandon this task?')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{pickAppText(locale, '放弃后会停止等待该任务的验收结果,此操作需要明确确认。', 'This stops waiting for this task acceptance result and requires confirmation.')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="gap-2 sm:gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setConfirmAbandonOpen(false)}
|
||||
className="h-11 rounded-md border border-border px-4 text-sm text-muted-foreground hover:bg-accent"
|
||||
>
|
||||
{pickAppText(locale, '取消', 'Cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setConfirmAbandonOpen(false);
|
||||
onFeedback(message.run_id!, 'abandon');
|
||||
}}
|
||||
className="h-11 rounded-md bg-destructive px-4 text-sm font-medium text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{pickAppText(locale, '确认放弃', 'Confirm abandon')}
|
||||
</button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{message.feedback_error && (
|
||||
<span className="text-xs text-destructive">{message.feedback_error}</span>
|
||||
)}
|
||||
@ -394,8 +437,8 @@ export function MessageList({
|
||||
})();
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full px-8" viewportRef={viewportRef}>
|
||||
<div className="mx-auto max-w-5xl space-y-8 py-10">
|
||||
<ScrollArea className="h-full px-3 sm:px-5 md:px-8" viewportRef={viewportRef}>
|
||||
<div className="mx-auto max-w-5xl space-y-8 py-6 md:py-10">
|
||||
{visibleMessages.length === 0 && teamGroups.length === 0 && !isThinking && (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
|
||||
<Bot className="w-12 h-12 mb-4 opacity-50" />
|
||||
|
||||
Reference in New Issue
Block a user