feat: optimize desktop client style

- light theme colors, fix chat cancel logic, add clear-context feedback, polish thinking step icon/order, and align channels empty state with the web console
This commit is contained in:
zhayujie
2026-07-16 19:31:56 +08:00
parent c7060c147d
commit 8b426ed71d
6 changed files with 73 additions and 22 deletions

View File

@@ -121,8 +121,15 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message, onRegenerate, on
muted, separated from the final answer by a dashed divider. */}
{(hasSteps || hasLiveReasoning) && (
<div className="mb-2.5 pb-2 border-b border-dashed border-default">
{hasLiveReasoning && <ThinkingStep content={message.reasoning!} streaming />}
{hasSteps && <MessageSteps steps={message.steps!} />}
{/* Live reasoning is the current, not-yet-committed thinking, so it
must render after all committed steps (tools/thinking), not at
the very top of the bubble. */}
{hasLiveReasoning && (
<div className={hasSteps ? 'mt-1' : ''}>
<ThinkingStep content={message.reasoning!} streaming />
</div>
)}
</div>
)}

View File

@@ -1,6 +1,7 @@
import React, { useState } from 'react'
import { ChevronRight, Loader2, Check, X, Brain } from 'lucide-react'
import { ChevronRight, Loader2, Check, X, Lightbulb } from 'lucide-react'
import type { MessageStep } from '../types'
import { t } from '../i18n'
import Markdown from './Markdown'
/**
@@ -16,8 +17,8 @@ const ThinkingStep: React.FC<{ content: string; streaming?: boolean }> = ({ cont
className="flex items-center gap-1.5 cursor-pointer hover:text-content-secondary select-none transition-colors"
onClick={() => setExpanded((v) => !v)}
>
<Brain size={12} className="flex-shrink-0" />
<span className="flex-1">{streaming ? 'Thinking…' : 'Thought for a moment'}</span>
<Lightbulb size={13} className={`flex-shrink-0 text-amber-400 ${streaming ? 'animate-pulse' : ''}`} />
<span className="flex-1">{streaming ? t('thinking_in_progress') : t('thinking_done')}</span>
<ChevronRight size={11} className={`transition-transform opacity-50 ${expanded ? 'rotate-90' : ''}`} />
</div>
{expanded && (

View File

@@ -129,14 +129,17 @@ const translations: Record<string, Record<string, string>> = {
msg_cancelled: '已中止',
msg_self_learned: '自主学习',
msg_stop: '停止',
thinking_in_progress: '思考中…',
thinking_done: '已深度思考',
chat_clear_context: '清除上下文',
context_cleared: '— 以上内容已从上下文中移除 —',
chat_load_earlier: '加载更早的消息',
chat_send: '发送',
chat_attach: '添加附件',
slash_hint: '输入 / 查看命令',
chat_welcome: '有什么可以帮你的?',
chat_empty_hint: '发送一条消息开始对话',
welcome_subtitle: '我可以帮你解问题、管理你的电脑、创建并执行技能,\n还能通过长期记忆不断成长。',
welcome_subtitle: '我可以帮你解问题、管理你的电脑、创建并执行技能,\n还能通过长期记忆不断成长。',
example_sys_title: '系统管理',
example_sys_text: '查看工作空间里有哪些文件',
example_task_title: '定时任务',
@@ -288,6 +291,8 @@ const translations: Record<string, Record<string, string>> = {
channels_connected_section: '已连接',
channels_available_section: '可添加',
channels_empty_connected: '暂无已连接的通道',
channels_empty: '暂未接入任何通道',
channels_empty_desc: '点击右上角「接入通道」按钮,即可将 CowAgent 接入微信、飞书、钉钉等消息通道',
channels_qr_hint: '该通道通过扫码登录,请前往 Web 控制台完成扫码连接',
channels_save_ok: '已保存',
channels_save_error: '保存失败',
@@ -512,7 +517,10 @@ const translations: Record<string, Record<string, string>> = {
msg_cancelled: 'Cancelled',
msg_self_learned: 'Self-learned',
msg_stop: 'Stop',
thinking_in_progress: 'Thinking…',
thinking_done: 'Thought',
chat_clear_context: 'Clear context',
context_cleared: '— Context above has been cleared —',
chat_load_earlier: 'Load earlier messages',
chat_send: 'Send',
chat_attach: 'Attach file',
@@ -671,6 +679,8 @@ const translations: Record<string, Record<string, string>> = {
channels_connected_section: 'Connected',
channels_available_section: 'Available',
channels_empty_connected: 'No connected channels yet',
channels_empty: 'No channels connected',
channels_empty_desc: 'Click "Add channel" above to connect CowAgent to WeChat, Feishu, DingTalk and more',
channels_qr_hint: 'This channel uses QR login — please connect it from the Web console',
channels_save_ok: 'Saved',
channels_save_error: 'Failed to save',

View File

@@ -27,21 +27,21 @@
--danger-border: rgba(239, 68, 68, 0.3);
--info: #3b82f6;
/* Light theme — aligned with the web console (Tailwind gray/slate palette) */
/* Light theme — aligned with the web console: gray surfaces/borders + slate text */
--bg-base: #f9fafb; /* app background (gray-50) */
--bg-surface: #ffffff; /* panels, cards */
--bg-surface-2: #f1f5f9; /* nested surfaces, hover fills (slate-100) */
--bg-surface-2: #f3f4f6; /* nested surfaces, hover fills (gray-100) */
--bg-elevated: #ffffff; /* popovers, menus, modals */
--bg-inset: #f1f5f9; /* inputs, code blocks (slate-100) */
--bg-inset: #f3f4f6; /* inputs, code blocks (gray-100) */
--text-primary: #1e293b; /* headings, primary text (slate-800) */
--text-secondary: #475569; /* body, labels (slate-600) */
--text-tertiary: #64748b; /* hints, captions (slate-500) */
--text-disabled: #94a3b8; /* slate-400 */
--border-default: #e2e8f0; /* slate-200 */
--border-strong: #cbd5e1; /* slate-300 */
--border-subtle: #f1f5f9; /* slate-100 */
--border-default: #e5e7eb; /* gray-200 (web console border) */
--border-strong: #d1d5db; /* gray-300 */
--border-subtle: #f3f4f6; /* gray-100 */
--overlay: rgba(0, 0, 0, 0.4);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0, 0, 0, 0.06);

View File

@@ -13,6 +13,7 @@ import {
Headset,
Hash,
AtSign,
RadioTower,
} from 'lucide-react'
import { t, localizedLabel } from '../i18n'
import apiClient from '../api/client'
@@ -147,7 +148,25 @@ const ChannelsPage: React.FC<ChannelsPageProps> = ({ baseUrl }) => {
) : (
<div className="space-y-3">
{connected.length === 0 && !addOpen ? (
<p className="text-sm text-content-tertiary py-2">{t('channels_empty_connected')}</p>
<div className="flex flex-col items-center justify-center text-center py-16 px-6">
<span className="w-16 h-16 rounded-2xl bg-info/10 flex items-center justify-center mb-4">
<RadioTower size={26} className="text-info" />
</span>
<p className="text-content-secondary font-medium">{t('channels_empty')}</p>
<p className="text-sm text-content-tertiary mt-1.5 max-w-sm leading-relaxed">
{t('channels_empty_desc')}
</p>
{available.length > 0 && (
<div className="mt-5">
<Btn variant="primary" onClick={openAdd}>
<span className="flex items-center gap-1.5">
<Plus size={15} />
{t('channels_add')}
</span>
</Btn>
</div>
)}
</div>
) : (
connected.map((ch) => <ChannelCard key={ch.name} channel={ch} onChanged={loadChannels} />)
)}

View File

@@ -278,16 +278,30 @@ const ChatPage: React.FC<ChatPageProps> = ({ baseUrl }) => {
</div>
) : (
<div className="py-3 max-w-3xl mx-auto">
{messages.map((msg) => (
<MessageBubble
key={msg.id}
message={msg}
onRegenerate={handleRegenerate}
onEdit={handleEdit}
onDelete={handleDelete}
onMediaLoad={handleMediaLoad}
/>
))}
{messages.map((msg) =>
msg.kind === 'divider' ? (
<div key={msg.id} className="flex items-center gap-3 px-6 py-3 text-content-tertiary">
<span
className="flex-1 h-px"
style={{ background: 'linear-gradient(to right, transparent, var(--border-strong), transparent)' }}
/>
<span className="text-xs whitespace-nowrap">{t('context_cleared')}</span>
<span
className="flex-1 h-px"
style={{ background: 'linear-gradient(to right, transparent, var(--border-strong), transparent)' }}
/>
</div>
) : (
<MessageBubble
key={msg.id}
message={msg}
onRegenerate={handleRegenerate}
onEdit={handleEdit}
onDelete={handleDelete}
onMediaLoad={handleMediaLoad}
/>
)
)}
<div ref={bottomRef} />
</div>
)}