This commit is contained in:
从何开始123
2026-01-08 12:56:00 +08:00
parent 1561c054b7
commit 37b0e5f8e6
13 changed files with 258 additions and 140 deletions

View File

@@ -24,67 +24,69 @@ const ChatArea = ({
processStartTime,
processEndTime
}: ChatAreaProps) => {
const isIdle = messages.length === 0 && appState === 'idle';
return (
<div className="flex-1 overflow-y-auto custom-scrollbar scroll-smooth">
<div className="pb-40">
{messages.length === 0 && appState === 'idle' && (
<div className="h-full flex flex-col items-center justify-center pt-32 opacity-70 px-4 text-center">
<Logo className="w-24 h-24 mb-6 drop-shadow-xl animate-pulse-slow" />
<p className="text-xl font-bold text-slate-900">Prisma</p>
<p className="text-sm text-slate-500 max-w-xs mt-2">
Deep multi-agent reasoning.
</p>
</div>
)}
{isIdle ? (
<div className="h-full flex flex-col items-center justify-center opacity-70 px-4 text-center">
<Logo className="w-24 h-24 mb-6 drop-shadow-xl animate-pulse-slow" />
<p className="text-xl font-bold text-slate-900">Prisma</p>
<p className="text-sm text-slate-500 max-w-xs mt-2">
Deep multi-agent reasoning.
</p>
</div>
) : (
<div className="pb-40">
{/* History */}
{messages.map((msg, idx) => (
<ChatMessageItem
key={msg.id}
message={msg}
isLast={idx === messages.length - 1}
/>
))}
{/* History */}
{messages.map((msg, idx) => (
<ChatMessageItem
key={msg.id}
message={msg}
isLast={idx === messages.length - 1}
/>
))}
{/* Active Generation (Ghost Message) */}
{appState !== 'idle' && appState !== 'completed' && (
<div className="group w-full bg-transparent text-slate-800">
{/* Active Generation (Ghost Message) */}
{appState !== 'idle' && appState !== 'completed' && (
<div className="group w-full bg-transparent text-slate-800">
<div className="max-w-6xl mx-auto px-4 py-8 flex gap-6">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-white border border-blue-200 shadow-sm flex items-center justify-center">
<div className="animate-spin w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full"></div>
</div>
<div className="flex-1 min-w-0">
<div className="font-semibold text-sm text-slate-900 mb-2">Prisma</div>
{/* Active Thinking Process */}
<div className="mb-4 bg-white border border-blue-100 rounded-xl p-4 shadow-sm">
<ProcessFlow
appState={appState}
managerAnalysis={managerAnalysis}
experts={experts}
processStartTime={processStartTime}
processEndTime={processEndTime}
/>
</div>
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-white border border-blue-200 shadow-sm flex items-center justify-center">
<div className="animate-spin w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full"></div>
</div>
<div className="flex-1 min-w-0">
<div className="font-semibold text-sm text-slate-900 mb-2">Prisma</div>
{/* Active Thinking Process */}
<div className="mb-4 bg-white border border-blue-100 rounded-xl p-4 shadow-sm">
<ProcessFlow
appState={appState}
managerAnalysis={managerAnalysis}
experts={experts}
processStartTime={processStartTime}
processEndTime={processEndTime}
/>
</div>
{/* Streaming Output */}
{finalOutput && (
<div className="prose prose-slate max-w-none">
<ChatMessageItem
message={{
id: 'streaming',
role: 'model',
content: finalOutput,
isThinking: false
}}
/>
</div>
)}
</div>
{/* Streaming Output */}
{finalOutput && (
<div className="prose prose-slate max-w-none">
<ChatMessageItem
message={{
id: 'streaming',
role: 'model',
content: finalOutput,
isThinking: false
}}
/>
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
);
};

View File

@@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { User, Sparkles, ChevronDown, ChevronRight } from 'lucide-react';
import { User, Sparkles, ChevronDown, ChevronRight, Copy, Check } from 'lucide-react';
import MarkdownRenderer from './MarkdownRenderer';
import ProcessFlow from './ProcessFlow';
import { ChatMessage } from '../types';
@@ -12,10 +13,18 @@ interface ChatMessageProps {
const ChatMessageItem = ({ message, isLast }: ChatMessageProps) => {
const isUser = message.role === 'user';
const [showThinking, setShowThinking] = useState(false);
const [copied, setCopied] = useState(false);
// Check if there is any thinking data to show
const hasThinkingData = message.analysis || (message.experts && message.experts.length > 0);
const handleCopy = () => {
if (!message.content) return;
navigator.clipboard.writeText(message.content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className={`group w-full text-slate-800 ${isUser ? 'bg-transparent' : 'bg-transparent'}`}>
<div className="max-w-6xl mx-auto px-4 py-8 flex gap-4 md:gap-6">
@@ -36,8 +45,30 @@ const ChatMessageItem = ({ message, isLast }: ChatMessageProps) => {
{/* Content */}
<div className="relative flex-1 overflow-hidden">
<div className="font-semibold text-sm text-slate-900 mb-1">
{isUser ? 'You' : 'Prisma'}
<div className="flex items-center justify-between mb-1">
<div className="font-semibold text-sm text-slate-900">
{isUser ? 'You' : 'Prisma'}
</div>
{message.content && (
<button
onClick={handleCopy}
className={`p-1.5 rounded-md transition-all duration-200 flex items-center gap-1.5
${copied
? 'text-emerald-600 bg-emerald-50'
: 'text-slate-400 hover:text-slate-600 hover:bg-slate-100 opacity-0 group-hover:opacity-100 focus:opacity-100'
}`}
title="Copy message"
>
{copied ? (
<>
<Check size={14} />
<span className="text-[10px] font-medium uppercase tracking-wider">Copied</span>
</>
) : (
<Copy size={14} />
)}
</button>
)}
</div>
{/* Thinking Process Accordion (Only for AI) */}
@@ -100,4 +131,4 @@ const ChatMessageItem = ({ message, isLast }: ChatMessageProps) => {
);
};
export default ChatMessageItem;
export default ChatMessageItem;

View File

@@ -15,7 +15,7 @@ interface HeaderProps {
const Header = ({ selectedModel, setSelectedModel, onOpenSettings, onToggleSidebar, onNewChat }: HeaderProps) => {
return (
<header className="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-100">
<header className="sticky top-0 z-50 bg-white/80 backdrop-blur-md">
<div className="w-full px-4 h-16 flex items-center justify-between">
<div className="flex items-center gap-4">
<button
@@ -65,4 +65,4 @@ const Header = ({ selectedModel, setSelectedModel, onOpenSettings, onToggleSideb
);
};
export default Header;
export default Header;

View File

@@ -1,3 +1,4 @@
import React, { useRef, useLayoutEffect, useState, useEffect } from 'react';
import { ArrowUp, Square } from 'lucide-react';
import { AppState } from '../types';
@@ -8,9 +9,10 @@ interface InputSectionProps {
onRun: () => void;
onStop: () => void;
appState: AppState;
focusTrigger?: number;
}
const InputSection = ({ query, setQuery, onRun, onStop, appState }: InputSectionProps) => {
const InputSection = ({ query, setQuery, onRun, onStop, appState, focusTrigger }: InputSectionProps) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [isComposing, setIsComposing] = useState(false);
@@ -35,11 +37,12 @@ const InputSection = ({ query, setQuery, onRun, onStop, appState }: InputSection
};
// Focus input on mount and when app becomes idle (e.g. after "New Chat" or completion)
// or when explicitly triggered by focusTrigger
useEffect(() => {
if (appState === 'idle' && textareaRef.current) {
textareaRef.current.focus();
}
}, [appState]);
}, [appState, focusTrigger]);
// useLayoutEffect prevents visual flickering by adjusting height before paint
useLayoutEffect(() => {
@@ -107,4 +110,4 @@ const InputSection = ({ query, setQuery, onRun, onStop, appState }: InputSection
);
};
export default InputSection;
export default InputSection;

View File

@@ -1,3 +1,4 @@
import React, { useState, useEffect } from 'react';
import { Users, Zap, Brain, Loader2, CheckCircle2, Clock } from 'lucide-react';
import { AppState, AnalysisResult, ExpertResult } from '../types';
@@ -66,7 +67,7 @@ const ProcessFlow = ({ appState, managerAnalysis, experts, defaultExpanded = tru
const expertsStatus = anyExpertWorking ? 'active' : (allExpertsDone ? 'completed' : 'idle');
return (
<div className="relative space-y-4 pt-4">
<div className="relative space-y-4 pt-4 w-full">
{/* Global Timer Overlay */}
<GlobalTimer start={processStartTime} end={processEndTime} appState={appState} />
@@ -115,7 +116,7 @@ const ProcessFlow = ({ appState, managerAnalysis, experts, defaultExpanded = tru
isExpanded={isExpanded}
onToggle={() => setIsExpanded(!isExpanded)}
>
<div className="grid grid-cols-1 gap-3 pt-2">
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 pt-2">
{experts.map((expert) => (
<ExpertCard key={expert.id} expert={expert} />
))}
@@ -152,4 +153,4 @@ const ProcessFlow = ({ appState, managerAnalysis, experts, defaultExpanded = tru
);
};
export default ProcessFlow;
export default ProcessFlow;