mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 11:07:11 +08:00
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:
@@ -121,8 +121,15 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message, onRegenerate, on
|
|||||||
muted, separated from the final answer by a dashed divider. */}
|
muted, separated from the final answer by a dashed divider. */}
|
||||||
{(hasSteps || hasLiveReasoning) && (
|
{(hasSteps || hasLiveReasoning) && (
|
||||||
<div className="mb-2.5 pb-2 border-b border-dashed border-default">
|
<div className="mb-2.5 pb-2 border-b border-dashed border-default">
|
||||||
{hasLiveReasoning && <ThinkingStep content={message.reasoning!} streaming />}
|
|
||||||
{hasSteps && <MessageSteps steps={message.steps!} />}
|
{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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from 'react'
|
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 type { MessageStep } from '../types'
|
||||||
|
import { t } from '../i18n'
|
||||||
import Markdown from './Markdown'
|
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"
|
className="flex items-center gap-1.5 cursor-pointer hover:text-content-secondary select-none transition-colors"
|
||||||
onClick={() => setExpanded((v) => !v)}
|
onClick={() => setExpanded((v) => !v)}
|
||||||
>
|
>
|
||||||
<Brain size={12} className="flex-shrink-0" />
|
<Lightbulb size={13} className={`flex-shrink-0 text-amber-400 ${streaming ? 'animate-pulse' : ''}`} />
|
||||||
<span className="flex-1">{streaming ? 'Thinking…' : 'Thought for a moment'}</span>
|
<span className="flex-1">{streaming ? t('thinking_in_progress') : t('thinking_done')}</span>
|
||||||
<ChevronRight size={11} className={`transition-transform opacity-50 ${expanded ? 'rotate-90' : ''}`} />
|
<ChevronRight size={11} className={`transition-transform opacity-50 ${expanded ? 'rotate-90' : ''}`} />
|
||||||
</div>
|
</div>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
|
|||||||
@@ -129,14 +129,17 @@ const translations: Record<string, Record<string, string>> = {
|
|||||||
msg_cancelled: '已中止',
|
msg_cancelled: '已中止',
|
||||||
msg_self_learned: '自主学习',
|
msg_self_learned: '自主学习',
|
||||||
msg_stop: '停止',
|
msg_stop: '停止',
|
||||||
|
thinking_in_progress: '思考中…',
|
||||||
|
thinking_done: '已深度思考',
|
||||||
chat_clear_context: '清除上下文',
|
chat_clear_context: '清除上下文',
|
||||||
|
context_cleared: '— 以上内容已从上下文中移除 —',
|
||||||
chat_load_earlier: '加载更早的消息',
|
chat_load_earlier: '加载更早的消息',
|
||||||
chat_send: '发送',
|
chat_send: '发送',
|
||||||
chat_attach: '添加附件',
|
chat_attach: '添加附件',
|
||||||
slash_hint: '输入 / 查看命令',
|
slash_hint: '输入 / 查看命令',
|
||||||
chat_welcome: '有什么可以帮你的?',
|
chat_welcome: '有什么可以帮你的?',
|
||||||
chat_empty_hint: '发送一条消息开始对话',
|
chat_empty_hint: '发送一条消息开始对话',
|
||||||
welcome_subtitle: '我可以帮你解答问题、管理你的电脑、创建并执行技能,\n还能通过长期记忆不断成长。',
|
welcome_subtitle: '我可以帮你解决问题、管理你的电脑、创建并执行技能,\n还能通过长期记忆不断成长。',
|
||||||
example_sys_title: '系统管理',
|
example_sys_title: '系统管理',
|
||||||
example_sys_text: '查看工作空间里有哪些文件',
|
example_sys_text: '查看工作空间里有哪些文件',
|
||||||
example_task_title: '定时任务',
|
example_task_title: '定时任务',
|
||||||
@@ -288,6 +291,8 @@ const translations: Record<string, Record<string, string>> = {
|
|||||||
channels_connected_section: '已连接',
|
channels_connected_section: '已连接',
|
||||||
channels_available_section: '可添加',
|
channels_available_section: '可添加',
|
||||||
channels_empty_connected: '暂无已连接的通道',
|
channels_empty_connected: '暂无已连接的通道',
|
||||||
|
channels_empty: '暂未接入任何通道',
|
||||||
|
channels_empty_desc: '点击右上角「接入通道」按钮,即可将 CowAgent 接入微信、飞书、钉钉等消息通道',
|
||||||
channels_qr_hint: '该通道通过扫码登录,请前往 Web 控制台完成扫码连接',
|
channels_qr_hint: '该通道通过扫码登录,请前往 Web 控制台完成扫码连接',
|
||||||
channels_save_ok: '已保存',
|
channels_save_ok: '已保存',
|
||||||
channels_save_error: '保存失败',
|
channels_save_error: '保存失败',
|
||||||
@@ -512,7 +517,10 @@ const translations: Record<string, Record<string, string>> = {
|
|||||||
msg_cancelled: 'Cancelled',
|
msg_cancelled: 'Cancelled',
|
||||||
msg_self_learned: 'Self-learned',
|
msg_self_learned: 'Self-learned',
|
||||||
msg_stop: 'Stop',
|
msg_stop: 'Stop',
|
||||||
|
thinking_in_progress: 'Thinking…',
|
||||||
|
thinking_done: 'Thought',
|
||||||
chat_clear_context: 'Clear context',
|
chat_clear_context: 'Clear context',
|
||||||
|
context_cleared: '— Context above has been cleared —',
|
||||||
chat_load_earlier: 'Load earlier messages',
|
chat_load_earlier: 'Load earlier messages',
|
||||||
chat_send: 'Send',
|
chat_send: 'Send',
|
||||||
chat_attach: 'Attach file',
|
chat_attach: 'Attach file',
|
||||||
@@ -671,6 +679,8 @@ const translations: Record<string, Record<string, string>> = {
|
|||||||
channels_connected_section: 'Connected',
|
channels_connected_section: 'Connected',
|
||||||
channels_available_section: 'Available',
|
channels_available_section: 'Available',
|
||||||
channels_empty_connected: 'No connected channels yet',
|
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_qr_hint: 'This channel uses QR login — please connect it from the Web console',
|
||||||
channels_save_ok: 'Saved',
|
channels_save_ok: 'Saved',
|
||||||
channels_save_error: 'Failed to save',
|
channels_save_error: 'Failed to save',
|
||||||
|
|||||||
@@ -27,21 +27,21 @@
|
|||||||
--danger-border: rgba(239, 68, 68, 0.3);
|
--danger-border: rgba(239, 68, 68, 0.3);
|
||||||
--info: #3b82f6;
|
--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-base: #f9fafb; /* app background (gray-50) */
|
||||||
--bg-surface: #ffffff; /* panels, cards */
|
--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-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-primary: #1e293b; /* headings, primary text (slate-800) */
|
||||||
--text-secondary: #475569; /* body, labels (slate-600) */
|
--text-secondary: #475569; /* body, labels (slate-600) */
|
||||||
--text-tertiary: #64748b; /* hints, captions (slate-500) */
|
--text-tertiary: #64748b; /* hints, captions (slate-500) */
|
||||||
--text-disabled: #94a3b8; /* slate-400 */
|
--text-disabled: #94a3b8; /* slate-400 */
|
||||||
|
|
||||||
--border-default: #e2e8f0; /* slate-200 */
|
--border-default: #e5e7eb; /* gray-200 (web console border) */
|
||||||
--border-strong: #cbd5e1; /* slate-300 */
|
--border-strong: #d1d5db; /* gray-300 */
|
||||||
--border-subtle: #f1f5f9; /* slate-100 */
|
--border-subtle: #f3f4f6; /* gray-100 */
|
||||||
|
|
||||||
--overlay: rgba(0, 0, 0, 0.4);
|
--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);
|
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Headset,
|
Headset,
|
||||||
Hash,
|
Hash,
|
||||||
AtSign,
|
AtSign,
|
||||||
|
RadioTower,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { t, localizedLabel } from '../i18n'
|
import { t, localizedLabel } from '../i18n'
|
||||||
import apiClient from '../api/client'
|
import apiClient from '../api/client'
|
||||||
@@ -147,7 +148,25 @@ const ChannelsPage: React.FC<ChannelsPageProps> = ({ baseUrl }) => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{connected.length === 0 && !addOpen ? (
|
{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} />)
|
connected.map((ch) => <ChannelCard key={ch.name} channel={ch} onChanged={loadChannels} />)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -278,7 +278,20 @@ const ChatPage: React.FC<ChatPageProps> = ({ baseUrl }) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="py-3 max-w-3xl mx-auto">
|
<div className="py-3 max-w-3xl mx-auto">
|
||||||
{messages.map((msg) => (
|
{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
|
<MessageBubble
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
message={msg}
|
message={msg}
|
||||||
@@ -287,7 +300,8 @@ const ChatPage: React.FC<ChatPageProps> = ({ baseUrl }) => {
|
|||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onMediaLoad={handleMediaLoad}
|
onMediaLoad={handleMediaLoad}
|
||||||
/>
|
/>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
<div ref={bottomRef} />
|
<div ref={bottomRef} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user