feat: release chat and mindspace UI updates
This commit is contained in:
@@ -8,6 +8,8 @@ import { filterText } from '../utils/wordFilter';
|
||||
import { formatChatTime, shouldShowTimestamp } from '../utils/time';
|
||||
import { TKMindAvatar } from './TKMindAvatar';
|
||||
import { UserAvatar } from './UserAvatar';
|
||||
import { ChatSharePreviewModal } from './ChatSharePreviewModal';
|
||||
import { ChatWelcomePanel } from './ChatWelcomePanel';
|
||||
|
||||
function CopyIcon() {
|
||||
return (
|
||||
@@ -37,18 +39,58 @@ function CheckIcon() {
|
||||
);
|
||||
}
|
||||
|
||||
function ToolSpinnerIcon() {
|
||||
return (
|
||||
<span className="tool-badge-spinner" aria-hidden="true">
|
||||
<span />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolDoneIcon() {
|
||||
return (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M5 12.5l4.5 4.5L19 7.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolBadge({ message }: { message: Message }) {
|
||||
const tools = message.content.filter((c) => c.type === 'toolRequest' || c.type === 'toolResponse');
|
||||
if (tools.length === 0) return null;
|
||||
|
||||
const completed = tools.filter((t) => t.type === 'toolResponse').length;
|
||||
const total = tools.length;
|
||||
|
||||
return (
|
||||
<div className="tool-badges">
|
||||
{tools.map((tool, i) => (
|
||||
<span key={`${message.id}-${i}`} className="tool-badge">
|
||||
{tool.type === 'toolRequest'
|
||||
? `🔧 ${tool.toolCall.functionName}`
|
||||
: `✓ 工具完成`}
|
||||
</span>
|
||||
))}
|
||||
{tools.map((tool, i) => {
|
||||
const isRequest = tool.type === 'toolRequest';
|
||||
const toolName = isRequest ? String(tool.toolCall.functionName ?? '').trim() : '';
|
||||
|
||||
let label: string;
|
||||
if (isRequest && !toolName) {
|
||||
label = `正在加载 tools (${completed}/${total})`;
|
||||
} else {
|
||||
label = isRequest ? toolName : '工具完成';
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
key={`${message.id}-${i}`}
|
||||
className={`tool-badge${isRequest ? ' is-pending' : ' is-complete'}`}
|
||||
>
|
||||
{isRequest ? <ToolSpinnerIcon /> : <ToolDoneIcon />}
|
||||
<span>{label}</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -126,6 +168,7 @@ function MessageRow({
|
||||
saveDisabled,
|
||||
publishUserId,
|
||||
publishUsername,
|
||||
sessionId,
|
||||
compact = false,
|
||||
}: {
|
||||
message: Message;
|
||||
@@ -135,9 +178,11 @@ function MessageRow({
|
||||
saveDisabled?: boolean;
|
||||
publishUserId?: string;
|
||||
publishUsername?: string;
|
||||
sessionId?: string;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
const [actionsOpen, setActionsOpen] = useState(false);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const rawText = getDisplayText(message);
|
||||
const text = filterText(rawText);
|
||||
const saveActions = getMessageSaveActions(text, { userId: publishUserId, username: publishUsername });
|
||||
@@ -227,15 +272,14 @@ function MessageRow({
|
||||
>
|
||||
{saveActions.kind === 'page' ? '保存页面' : '保存为文章'}
|
||||
</button>
|
||||
{saveActions.previewUrl && (
|
||||
<a
|
||||
{saveActions.previewUrl && sessionId && message.id && (
|
||||
<button
|
||||
type="button"
|
||||
className="msg-open-preview"
|
||||
href={saveActions.previewUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={() => setPreviewOpen(true)}
|
||||
>
|
||||
打开预览
|
||||
</a>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -254,15 +298,14 @@ function MessageRow({
|
||||
>
|
||||
{saveActions.kind === 'page' ? '保存页面' : '保存为文章'}
|
||||
</button>
|
||||
{saveActions.previewUrl && (
|
||||
<a
|
||||
{saveActions.previewUrl && sessionId && message.id && (
|
||||
<button
|
||||
type="button"
|
||||
className="msg-open-preview"
|
||||
href={saveActions.previewUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={() => setPreviewOpen(true)}
|
||||
>
|
||||
打开预览
|
||||
</a>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -278,6 +321,14 @@ function MessageRow({
|
||||
title={onAvatarClick ? '点击更换头像' : undefined}
|
||||
/>
|
||||
)}
|
||||
{previewOpen && sessionId && message.id && (
|
||||
<ChatSharePreviewModal
|
||||
sessionId={sessionId}
|
||||
messageId={message.id}
|
||||
onClose={() => setPreviewOpen(false)}
|
||||
onSave={onSaveAsPage ? () => onSaveAsPage(message) : undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -289,6 +340,7 @@ export function MessageList({
|
||||
onSaveAsPage,
|
||||
publishUserId,
|
||||
publishUsername,
|
||||
sessionId,
|
||||
compact = false,
|
||||
}: {
|
||||
messages: Message[];
|
||||
@@ -297,19 +349,14 @@ export function MessageList({
|
||||
onSaveAsPage?: (message: Message) => void;
|
||||
publishUserId?: string;
|
||||
publishUsername?: string;
|
||||
sessionId?: string;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
const { avatarUrl } = useUserAvatar();
|
||||
|
||||
return (
|
||||
<div className="message-list">
|
||||
{messages.length === 0 && (
|
||||
<div className={`empty-state${compact ? ' empty-state-compact' : ''}`}>
|
||||
<TKMindAvatar />
|
||||
<h2>TKMind</h2>
|
||||
<p>{compact ? '继续和空间里的 Agent 对话' : '发送消息即可在本机执行 Agent 任务'}</p>
|
||||
</div>
|
||||
)}
|
||||
{messages.length === 0 && <ChatWelcomePanel compact={compact} />}
|
||||
{messages.map((message, index) => {
|
||||
const prev = index > 0 ? messages[index - 1] : undefined;
|
||||
const showTime = shouldShowTimestamp(message.created, prev?.created);
|
||||
@@ -324,6 +371,7 @@ export function MessageList({
|
||||
saveDisabled={streaming}
|
||||
publishUserId={publishUserId}
|
||||
publishUsername={publishUsername}
|
||||
sessionId={sessionId}
|
||||
compact={compact}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user