feat: 界面全部改为中文

- 侧边栏导航:仪表盘、登录、内容浏览、发布、互动、API 测试、设置
- 7 个页面所有按钮、标签、提示、错误信息改为中文
- API 端点列表分类改为中文(登录、内容、发布、互动)
- 组件内文本:展开/收起、复制、点赞、收藏、评论等
- 页面标题改为 Social MCP - 管理后台
This commit is contained in:
2026-03-01 16:52:57 +08:00
parent 0e693842a6
commit 69a0f7b24c
16 changed files with 233 additions and 233 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Social MCP - Admin Dashboard</title> <title>Social MCP - 管理后台</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🤖</text></svg>" /> <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🤖</text></svg>" />
</head> </head>
<body class="bg-dark-bg text-dark-text"> <body class="bg-dark-bg text-dark-text">
+1 -1
View File
@@ -25,7 +25,7 @@ export function CommentTree({ comments, depth = 0 }: Props) {
</div> </div>
<p className="text-sm text-dark-text/90">{comment.content}</p> <p className="text-sm text-dark-text/90">{comment.content}</p>
{comment.likeCount > 0 && ( {comment.likeCount > 0 && (
<span className="text-xs text-dark-muted mt-1 block">{comment.likeCount} likes</span> <span className="text-xs text-dark-muted mt-1 block">{comment.likeCount} </span>
)} )}
</div> </div>
</div> </div>
+4 -4
View File
@@ -23,15 +23,15 @@ export function FeedCard({ feed, onClick }: Props) {
/> />
) : ( ) : (
<div className="w-full h-full flex items-center justify-center text-dark-muted text-sm"> <div className="w-full h-full flex items-center justify-center text-dark-muted text-sm">
No Cover
</div> </div>
)} )}
{feed.type === 'video' && ( {feed.type === 'video' && (
<Badge variant="info" className="absolute top-2 right-2">Video</Badge> <Badge variant="info" className="absolute top-2 right-2"></Badge>
)} )}
</div> </div>
<div className="p-3"> <div className="p-3">
<h3 className="text-sm font-medium line-clamp-2 mb-2">{feed.title || feed.description || 'Untitled'}</h3> <h3 className="text-sm font-medium line-clamp-2 mb-2">{feed.title || feed.description || '无标题'}</h3>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
{feed.user.avatar && ( {feed.user.avatar && (
@@ -39,7 +39,7 @@ export function FeedCard({ feed, onClick }: Props) {
)} )}
<span className="text-xs text-dark-muted truncate">{feed.user.nickname}</span> <span className="text-xs text-dark-muted truncate">{feed.user.nickname}</span>
</div> </div>
<span className="text-xs text-dark-muted shrink-0">{formatNumber(feed.likeCount)} likes</span> <span className="text-xs text-dark-muted shrink-0">{formatNumber(feed.likeCount)} </span>
</div> </div>
</div> </div>
</div> </div>
+9 -9
View File
@@ -40,7 +40,7 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
<div className="absolute inset-0 bg-black/60" onClick={onClose} /> <div className="absolute inset-0 bg-black/60" onClick={onClose} />
<div className="relative ml-auto w-full max-w-2xl bg-dark-card border-l border-dark-border overflow-y-auto"> <div className="relative ml-auto w-full max-w-2xl bg-dark-card border-l border-dark-border overflow-y-auto">
<div className="sticky top-0 bg-dark-card border-b border-dark-border px-5 py-3 flex items-center justify-between z-10"> <div className="sticky top-0 bg-dark-card border-b border-dark-border px-5 py-3 flex items-center justify-between z-10">
<h3 className="font-semibold truncate">Feed Detail</h3> <h3 className="font-semibold truncate"></h3>
<button onClick={onClose} className="text-dark-muted hover:text-dark-text text-xl">&times;</button> <button onClick={onClose} className="text-dark-muted hover:text-dark-text text-xl">&times;</button>
</div> </div>
@@ -85,7 +85,7 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
{/* Video */} {/* Video */}
{detail.videoUrl && ( {detail.videoUrl && (
<div className="rounded-xl overflow-hidden bg-dark-bg p-4"> <div className="rounded-xl overflow-hidden bg-dark-bg p-4">
<Badge variant="info">Video Note</Badge> <Badge variant="info"></Badge>
<p className="text-xs text-dark-muted mt-2 break-all">{detail.videoUrl}</p> <p className="text-xs text-dark-muted mt-2 break-all">{detail.videoUrl}</p>
</div> </div>
)} )}
@@ -108,10 +108,10 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-4 gap-3"> <div className="grid grid-cols-4 gap-3">
{[ {[
{ label: 'Likes', value: detail.likeCount }, { label: '点赞', value: detail.likeCount },
{ label: 'Collects', value: detail.collectCount }, { label: '收藏', value: detail.collectCount },
{ label: 'Comments', value: detail.commentCount }, { label: '评论', value: detail.commentCount },
{ label: 'Shares', value: detail.shareCount }, { label: '分享', value: detail.shareCount },
].map((s) => ( ].map((s) => (
<div key={s.label} className="bg-dark-bg rounded-lg p-3 text-center"> <div key={s.label} className="bg-dark-bg rounded-lg p-3 text-center">
<p className="text-lg font-bold">{formatNumber(s.value)}</p> <p className="text-lg font-bold">{formatNumber(s.value)}</p>
@@ -145,14 +145,14 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
variant="ghost" variant="ghost"
onClick={() => void navigator.clipboard.writeText(detail.id)} onClick={() => void navigator.clipboard.writeText(detail.id)}
> >
Copy Feed ID Feed ID
</Button> </Button>
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
onClick={() => void navigator.clipboard.writeText(detail.xsecToken)} onClick={() => void navigator.clipboard.writeText(detail.xsecToken)}
> >
Copy Token Token
</Button> </Button>
</div> </div>
</div> </div>
@@ -161,7 +161,7 @@ export function FeedDetail({ feedId, xsecToken, onClose, onUserClick }: Props) {
{detail.comments.length > 0 && ( {detail.comments.length > 0 && (
<div> <div>
<h3 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3"> <h3 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">
Comments ({detail.comments.length}) ({detail.comments.length})
</h3> </h3>
<CommentTree comments={detail.comments} /> <CommentTree comments={detail.comments} />
</div> </div>
+1 -1
View File
@@ -9,7 +9,7 @@ interface Props {
emptyText?: string; emptyText?: string;
} }
export function FeedGrid({ feeds, loading, onSelect, emptyText = 'No feeds found' }: Props) { export function FeedGrid({ feeds, loading, onSelect, emptyText = '暂无内容' }: Props) {
if (loading) { if (loading) {
return ( return (
<div className="flex justify-center py-12"> <div className="flex justify-center py-12">
+5 -5
View File
@@ -56,10 +56,10 @@ export function UserCard({ userId, xsecToken, onFeedSelect }: Props) {
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-4 gap-3"> <div className="grid grid-cols-4 gap-3">
{[ {[
{ label: 'Follows', value: profile.follows }, { label: '关注', value: profile.follows },
{ label: 'Fans', value: profile.fans }, { label: '粉丝', value: profile.fans },
{ label: 'Interactions', value: profile.interaction }, { label: '获赞与收藏', value: profile.interaction },
{ label: 'Notes', value: profile.feedCount }, { label: '笔记', value: profile.feedCount },
].map((s) => ( ].map((s) => (
<div key={s.label} className="bg-dark-bg rounded-lg p-3 text-center"> <div key={s.label} className="bg-dark-bg rounded-lg p-3 text-center">
<p className="text-lg font-bold">{formatNumber(s.value)}</p> <p className="text-lg font-bold">{formatNumber(s.value)}</p>
@@ -72,7 +72,7 @@ export function UserCard({ userId, xsecToken, onFeedSelect }: Props) {
{profile.feeds.length > 0 && ( {profile.feeds.length > 0 && (
<div> <div>
<h3 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3"> <h3 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">
Recent Notes ({profile.feeds.length}) ({profile.feeds.length})
</h3> </h3>
<FeedGrid feeds={profile.feeds} loading={false} onSelect={onFeedSelect} /> <FeedGrid feeds={profile.feeds} loading={false} onSelect={onFeedSelect} />
</div> </div>
+3 -3
View File
@@ -20,17 +20,17 @@ export function Header() {
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" /> <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
<line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" /> <line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" />
</svg> </svg>
Token Settings Token
</button> </button>
)} )}
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{health && ( {health && (
<Badge variant={health.healthy ? 'success' : 'danger'}> <Badge variant={health.healthy ? 'success' : 'danger'}>
{health.healthy ? 'Healthy' : 'Unhealthy'} {health.healthy ? '正常' : '异常'}
</Badge> </Badge>
)} )}
{!health && <Badge variant="warning">Connecting...</Badge>} {!health && <Badge variant="warning">...</Badge>}
</div> </div>
</header> </header>
); );
+2 -2
View File
@@ -17,13 +17,13 @@ export function JsonViewer({ data, collapsed = false, maxHeight = '400px' }: Pro
onClick={() => setIsCollapsed(!isCollapsed)} onClick={() => setIsCollapsed(!isCollapsed)}
className="text-xs text-dark-muted hover:text-dark-text" className="text-xs text-dark-muted hover:text-dark-text"
> >
{isCollapsed ? 'Expand' : 'Collapse'} {isCollapsed ? '展开' : '收起'}
</button> </button>
<button <button
onClick={() => void navigator.clipboard.writeText(json)} onClick={() => void navigator.clipboard.writeText(json)}
className="text-xs text-dark-muted hover:text-dark-accent" className="text-xs text-dark-muted hover:text-dark-accent"
> >
Copy
</button> </button>
</div> </div>
{!isCollapsed && ( {!isCollapsed && (
+20 -20
View File
@@ -1,25 +1,25 @@
export const NAV_ITEMS = [ export const NAV_ITEMS = [
{ path: '/', label: 'Dashboard', icon: 'dashboard' }, { path: '/', label: '仪表盘', icon: 'dashboard' },
{ path: '/login', label: 'Login', icon: 'login' }, { path: '/login', label: '登录', icon: 'login' },
{ path: '/browser', label: 'Browser', icon: 'browser' }, { path: '/browser', label: '内容浏览', icon: 'browser' },
{ path: '/publish', label: 'Publish', icon: 'publish' }, { path: '/publish', label: '发布', icon: 'publish' },
{ path: '/interactions', label: 'Interactions', icon: 'interactions' }, { path: '/interactions', label: '互动', icon: 'interactions' },
{ path: '/api-tester', label: 'API Tester', icon: 'api' }, { path: '/api-tester', label: 'API 测试', icon: 'api' },
{ path: '/settings', label: 'Settings', icon: 'settings' }, { path: '/settings', label: '设置', icon: 'settings' },
] as const; ] as const;
export const API_ENDPOINTS = [ export const API_ENDPOINTS = [
{ key: 'login_status', method: 'GET', path: '/api/xhs/login/status', label: 'Check Login Status', category: 'Login' }, { key: 'login_status', method: 'GET', path: '/api/xhs/login/status', label: '检查登录状态', category: '登录' },
{ key: 'login_qrcode', method: 'GET', path: '/api/xhs/login/qrcode', label: 'Get Login QR Code', category: 'Login' }, { key: 'login_qrcode', method: 'GET', path: '/api/xhs/login/qrcode', label: '获取登录二维码', category: '登录' },
{ key: 'login_delete', method: 'DELETE', path: '/api/xhs/login/cookies', label: 'Delete Cookies (Logout)', category: 'Login' }, { key: 'login_delete', method: 'DELETE', path: '/api/xhs/login/cookies', label: '删除 Cookie(登出)', category: '登录' },
{ key: 'feeds', method: 'GET', path: '/api/xhs/feeds', label: 'List Feeds', category: 'Content' }, { key: 'feeds', method: 'GET', path: '/api/xhs/feeds', label: '获取推荐列表', category: '内容' },
{ key: 'search', method: 'POST', path: '/api/xhs/search', label: 'Search', category: 'Content', body: { keyword: '', filters: { sort: 'general', type: 'all', time: 'all' } } }, { key: 'search', method: 'POST', path: '/api/xhs/search', label: '搜索', category: '内容', body: { keyword: '', filters: { sort: 'general', type: 'all', time: 'all' } } },
{ key: 'feed_detail', method: 'POST', path: '/api/xhs/feeds/detail', label: 'Feed Detail', category: 'Content', body: { feed_id: '', xsec_token: '', load_all_comments: false } }, { key: 'feed_detail', method: 'POST', path: '/api/xhs/feeds/detail', label: '笔记详情', category: '内容', body: { feed_id: '', xsec_token: '', load_all_comments: false } },
{ key: 'user_profile', method: 'POST', path: '/api/xhs/user/profile', label: 'User Profile', category: 'Content', body: { user_id: '', xsec_token: '' } }, { key: 'user_profile', method: 'POST', path: '/api/xhs/user/profile', label: '用户主页', category: '内容', body: { user_id: '', xsec_token: '' } },
{ key: 'publish_image', method: 'POST', path: '/api/xhs/publish/image', label: 'Publish Image Note', category: 'Publish', body: { title: '', content: '', images: [], tags: [], is_original: false, visibility: 'public' } }, { key: 'publish_image', method: 'POST', path: '/api/xhs/publish/image', label: '发布图文笔记', category: '发布', body: { title: '', content: '', images: [], tags: [], is_original: false, visibility: 'public' } },
{ key: 'publish_video', method: 'POST', path: '/api/xhs/publish/video', label: 'Publish Video Note', category: 'Publish', body: { title: '', content: '', video: '', tags: [], visibility: 'public' } }, { key: 'publish_video', method: 'POST', path: '/api/xhs/publish/video', label: '发布视频笔记', category: '发布', body: { title: '', content: '', video: '', tags: [], visibility: 'public' } },
{ key: 'comment', method: 'POST', path: '/api/xhs/comment', label: 'Post Comment', category: 'Interaction', body: { feed_id: '', xsec_token: '', content: '' } }, { key: 'comment', method: 'POST', path: '/api/xhs/comment', label: '发表评论', category: '互动', body: { feed_id: '', xsec_token: '', content: '' } },
{ key: 'comment_reply', method: 'POST', path: '/api/xhs/comment/reply', label: 'Reply Comment', category: 'Interaction', body: { feed_id: '', xsec_token: '', content: '', comment_id: '', user_id: '' } }, { key: 'comment_reply', method: 'POST', path: '/api/xhs/comment/reply', label: '回复评论', category: '互动', body: { feed_id: '', xsec_token: '', content: '', comment_id: '', user_id: '' } },
{ key: 'like', method: 'POST', path: '/api/xhs/like', label: 'Like/Unlike', category: 'Interaction', body: { feed_id: '', xsec_token: '', unlike: false } }, { key: 'like', method: 'POST', path: '/api/xhs/like', label: '点赞/取消', category: '互动', body: { feed_id: '', xsec_token: '', unlike: false } },
{ key: 'favorite', method: 'POST', path: '/api/xhs/favorite', label: 'Favorite/Unfavorite', category: 'Interaction', body: { feed_id: '', xsec_token: '', unfavorite: false } }, { key: 'favorite', method: 'POST', path: '/api/xhs/favorite', label: '收藏/取消', category: '互动', body: { feed_id: '', xsec_token: '', unfavorite: false } },
] as const; ] as const;
+26 -26
View File
@@ -52,12 +52,12 @@ export function ApiTesterPage() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<h1 className="text-2xl font-bold">API Tester</h1> <h1 className="text-2xl font-bold">API </h1>
<Tabs <Tabs
tabs={[ tabs={[
{ key: 'rest', label: 'REST API' }, { key: 'rest', label: 'REST API' },
{ key: 'mcp', label: 'MCP Tools' }, { key: 'mcp', label: 'MCP 工具' },
]} ]}
active={mode} active={mode}
onChange={(k) => setMode(k as 'rest' | 'mcp')} onChange={(k) => setMode(k as 'rest' | 'mcp')}
@@ -87,11 +87,11 @@ export function ApiTesterPage() {
{response !== null && ( {response !== null && (
<Card> <Card>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider">Response</h2> <h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider"></h2>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{responseStatus && ( {responseStatus && (
<Badge variant={responseStatus === 'success' ? 'success' : 'danger'}> <Badge variant={responseStatus === 'success' ? 'success' : 'danger'}>
{responseStatus} {responseStatus === 'success' ? '成功' : '失败'}
</Badge> </Badge>
)} )}
{duration !== null && ( {duration !== null && (
@@ -108,11 +108,11 @@ export function ApiTesterPage() {
<div> <div>
<Card> <Card>
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider">History</h2> <h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider"></h2>
<Button variant="ghost" size="sm" onClick={() => setHistory([])}>Clear</Button> <Button variant="ghost" size="sm" onClick={() => setHistory([])}></Button>
</div> </div>
{history.length === 0 ? ( {history.length === 0 ? (
<p className="text-sm text-dark-muted">No requests yet</p> <p className="text-sm text-dark-muted"></p>
) : ( ) : (
<div className="space-y-2 max-h-[600px] overflow-y-auto"> <div className="space-y-2 max-h-[600px] overflow-y-auto">
{history.map((entry) => ( {history.map((entry) => (
@@ -136,7 +136,7 @@ export function ApiTesterPage() {
</div> </div>
<div className="flex items-center gap-2 mt-1 text-dark-muted"> <div className="flex items-center gap-2 mt-1 text-dark-muted">
<Badge variant={entry.status === 'success' ? 'success' : 'danger'} className="text-[10px]"> <Badge variant={entry.status === 'success' ? 'success' : 'danger'} className="text-[10px]">
{entry.status} {entry.status === 'success' ? '成功' : '失败'}
</Badge> </Badge>
<span>{entry.time}</span> <span>{entry.time}</span>
<span>{entry.duration}ms</span> <span>{entry.duration}ms</span>
@@ -227,10 +227,10 @@ function RestPanel({
return ( return (
<> <>
<Card> <Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">REST Request</h2> <h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">REST </h2>
<div className="space-y-3"> <div className="space-y-3">
<Select <Select
label="Endpoint" label="端点"
options={categories.flatMap((cat) => [ options={categories.flatMap((cat) => [
{ value: `__cat_${cat}`, label: `── ${cat} ──` }, { value: `__cat_${cat}`, label: `── ${cat} ──` },
...API_ENDPOINTS.filter((e) => e.category === cat).map((e) => ({ ...API_ENDPOINTS.filter((e) => e.category === cat).map((e) => ({
@@ -253,7 +253,7 @@ function RestPanel({
</div> </div>
{endpoint.method !== 'GET' && ( {endpoint.method !== 'GET' && (
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<label className="text-sm text-dark-muted">Request Body (JSON)</label> <label className="text-sm text-dark-muted"> (JSON)</label>
<textarea <textarea
value={bodyText} value={bodyText}
onChange={(e) => setBodyText(e.target.value)} onChange={(e) => setBodyText(e.target.value)}
@@ -263,8 +263,8 @@ function RestPanel({
</div> </div>
)} )}
<div className="flex gap-2"> <div className="flex gap-2">
<Button onClick={() => void handleSend()} loading={loading}>Send Request</Button> <Button onClick={() => void handleSend()} loading={loading}></Button>
<Button variant="ghost" size="sm" onClick={() => void navigator.clipboard.writeText(curl)}>Copy cURL</Button> <Button variant="ghost" size="sm" onClick={() => void navigator.clipboard.writeText(curl)}> cURL</Button>
</div> </div>
</div> </div>
</Card> </Card>
@@ -375,20 +375,20 @@ function McpPanel({
<> <>
{/* Connection */} {/* Connection */}
<Card> <Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">MCP Connection</h2> <h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">MCP </h2>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Badge variant={connState === 'connected' ? 'success' : connState === 'connecting' ? 'warning' : 'default'}> <Badge variant={connState === 'connected' ? 'success' : connState === 'connecting' ? 'warning' : 'default'}>
{connState} {connState === 'connected' ? '已连接' : connState === 'connecting' ? '连接中' : '未连接'}
</Badge> </Badge>
{connState === 'disconnected' && ( {connState === 'disconnected' && (
<Button size="sm" onClick={() => void handleConnect()} loading={connecting}> <Button size="sm" onClick={() => void handleConnect()} loading={connecting}>
Connect
</Button> </Button>
)} )}
{connState === 'connected' && ( {connState === 'connected' && (
<> <>
<span className="text-xs text-dark-muted">{tools.length} tools available</span> <span className="text-xs text-dark-muted">{tools.length} </span>
<Button size="sm" variant="ghost" onClick={handleDisconnect}>Disconnect</Button> <Button size="sm" variant="ghost" onClick={handleDisconnect}></Button>
</> </>
)} )}
{connState === 'connecting' && <Spinner size="sm" />} {connState === 'connecting' && <Spinner size="sm" />}
@@ -398,10 +398,10 @@ function McpPanel({
{/* Tool Call */} {/* Tool Call */}
{connState === 'connected' && tools.length > 0 && ( {connState === 'connected' && tools.length > 0 && (
<Card> <Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">MCP Tool Call</h2> <h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">MCP </h2>
<div className="space-y-3"> <div className="space-y-3">
<Select <Select
label="Tool" label="工具"
options={tools.map((t) => ({ value: t.name, label: t.name }))} options={tools.map((t) => ({ value: t.name, label: t.name }))}
value={selectedTool} value={selectedTool}
onChange={(e) => handleToolChange(e.target.value)} onChange={(e) => handleToolChange(e.target.value)}
@@ -415,13 +415,13 @@ function McpPanel({
)} )}
{currentTool.inputSchema?.properties && ( {currentTool.inputSchema?.properties && (
<div> <div>
<p className="text-dark-muted font-semibold mb-1">Parameters:</p> <p className="text-dark-muted font-semibold mb-1"></p>
{Object.entries(currentTool.inputSchema.properties).map(([key, prop]) => ( {Object.entries(currentTool.inputSchema.properties).map(([key, prop]) => (
<div key={key} className="flex gap-2 ml-2"> <div key={key} className="flex gap-2 ml-2">
<code className="text-dark-accent">{key}</code> <code className="text-dark-accent">{key}</code>
<span className="text-dark-muted"> <span className="text-dark-muted">
{prop.type || ''} {prop.type || ''}
{currentTool.inputSchema?.required?.includes(key) ? ' (required)' : ' (optional)'} {currentTool.inputSchema?.required?.includes(key) ? ' (必填)' : ' (可选)'}
{prop.description ? `${prop.description}` : ''} {prop.description ? `${prop.description}` : ''}
</span> </span>
</div> </div>
@@ -433,7 +433,7 @@ function McpPanel({
{/* Arguments editor */} {/* Arguments editor */}
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<label className="text-sm text-dark-muted">Arguments (JSON)</label> <label className="text-sm text-dark-muted"> (JSON)</label>
<textarea <textarea
value={argsText} value={argsText}
onChange={(e) => setArgsText(e.target.value)} onChange={(e) => setArgsText(e.target.value)}
@@ -443,7 +443,7 @@ function McpPanel({
</div> </div>
<Button onClick={() => void handleCall()} loading={loading}> <Button onClick={() => void handleCall()} loading={loading}>
Call Tool
</Button> </Button>
</div> </div>
</Card> </Card>
@@ -452,8 +452,8 @@ function McpPanel({
{connState === 'disconnected' && ( {connState === 'disconnected' && (
<Card> <Card>
<div className="text-center py-8 text-dark-muted"> <div className="text-center py-8 text-dark-muted">
<p>Connect to the MCP server to discover and test tools.</p> <p> MCP </p>
<p className="text-xs mt-2">This uses the same SSE + JSON-RPC protocol that AI clients use.</p> <p className="text-xs mt-2">使 AI SSE + JSON-RPC </p>
</div> </div>
</Card> </Card>
)} )}
+33 -33
View File
@@ -43,10 +43,10 @@ export function BrowserPage() {
if (res.success && res.data) { if (res.success && res.data) {
setFeeds(res.data); setFeeds(res.data);
} else { } else {
toast('error', res.error?.message || 'Failed to load feeds'); toast('error', res.error?.message || '加载推荐失败');
} }
} catch (err) { } catch (err) {
toast('error', err instanceof Error ? err.message : 'Failed to load feeds'); toast('error', err instanceof Error ? err.message : '加载推荐失败');
} finally { } finally {
setFeedsLoading(false); setFeedsLoading(false);
} }
@@ -54,7 +54,7 @@ export function BrowserPage() {
const handleSearch = useCallback(async () => { const handleSearch = useCallback(async () => {
if (!keyword.trim()) { if (!keyword.trim()) {
toast('warning', 'Enter a keyword'); toast('warning', '请输入关键词');
return; return;
} }
setSearchLoading(true); setSearchLoading(true);
@@ -68,10 +68,10 @@ export function BrowserPage() {
if (res.success && res.data) { if (res.success && res.data) {
setSearchResults(res.data); setSearchResults(res.data);
} else { } else {
toast('error', res.error?.message || 'Search failed'); toast('error', res.error?.message || '搜索失败');
} }
} catch (err) { } catch (err) {
toast('error', err instanceof Error ? err.message : 'Search failed'); toast('error', err instanceof Error ? err.message : '搜索失败');
} finally { } finally {
setSearchLoading(false); setSearchLoading(false);
} }
@@ -89,13 +89,13 @@ export function BrowserPage() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<h1 className="text-2xl font-bold">Content Browser</h1> <h1 className="text-2xl font-bold"></h1>
<Tabs <Tabs
tabs={[ tabs={[
{ key: 'explore', label: 'Explore' }, { key: 'explore', label: '探索' },
{ key: 'search', label: 'Search' }, { key: 'search', label: '搜索' },
{ key: 'user', label: 'User Profile' }, { key: 'user', label: '用户主页' },
]} ]}
active={tab} active={tab}
onChange={(k) => setTab(k as TabKey)} onChange={(k) => setTab(k as TabKey)}
@@ -105,13 +105,13 @@ export function BrowserPage() {
{tab === 'explore' && ( {tab === 'explore' && (
<div className="space-y-4"> <div className="space-y-4">
<Button onClick={() => void handleExplore()} loading={feedsLoading}> <Button onClick={() => void handleExplore()} loading={feedsLoading}>
Load Feed
</Button> </Button>
<FeedGrid <FeedGrid
feeds={feeds} feeds={feeds}
loading={feedsLoading} loading={feedsLoading}
onSelect={handleFeedSelect} onSelect={handleFeedSelect}
emptyText="Click 'Load Feed' to get recommended content" emptyText="点击「加载推荐」获取推荐内容"
/> />
</div> </div>
)} )}
@@ -122,53 +122,53 @@ export function BrowserPage() {
<div className="flex gap-3 items-end flex-wrap"> <div className="flex gap-3 items-end flex-wrap">
<div className="flex-1 min-w-[200px]"> <div className="flex-1 min-w-[200px]">
<Input <Input
label="Keyword" label="关键词"
placeholder="Search xiaohongshu..." placeholder="搜索小红书..."
value={keyword} value={keyword}
onChange={(e) => setKeyword(e.target.value)} onChange={(e) => setKeyword(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && void handleSearch()} onKeyDown={(e) => e.key === 'Enter' && void handleSearch()}
/> />
</div> </div>
<Select <Select
label="Sort" label="排序"
options={[ options={[
{ value: 'general', label: 'Default' }, { value: 'general', label: '默认' },
{ value: 'time_descending', label: 'Latest' }, { value: 'time_descending', label: '最新' },
{ value: 'popularity_descending', label: 'Popular' }, { value: 'popularity_descending', label: '热门' },
]} ]}
value={sortFilter} value={sortFilter}
onChange={(e) => setSortFilter(e.target.value)} onChange={(e) => setSortFilter(e.target.value)}
/> />
<Select <Select
label="Type" label="类型"
options={[ options={[
{ value: 'all', label: 'All' }, { value: 'all', label: '全部' },
{ value: 'note', label: 'Notes' }, { value: 'note', label: '图文' },
{ value: 'video', label: 'Videos' }, { value: 'video', label: '视频' },
]} ]}
value={typeFilter} value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)} onChange={(e) => setTypeFilter(e.target.value)}
/> />
<Select <Select
label="Time" label="时间"
options={[ options={[
{ value: 'all', label: 'Any time' }, { value: 'all', label: '不限' },
{ value: 'day', label: 'Past day' }, { value: 'day', label: '一天内' },
{ value: 'week', label: 'Past week' }, { value: 'week', label: '一周内' },
{ value: 'half_year', label: 'Past 6 months' }, { value: 'half_year', label: '半年内' },
]} ]}
value={timeFilter} value={timeFilter}
onChange={(e) => setTimeFilter(e.target.value)} onChange={(e) => setTimeFilter(e.target.value)}
/> />
<Button onClick={() => void handleSearch()} loading={searchLoading}> <Button onClick={() => void handleSearch()} loading={searchLoading}>
Search
</Button> </Button>
</div> </div>
<FeedGrid <FeedGrid
feeds={searchResults} feeds={searchResults}
loading={searchLoading} loading={searchLoading}
onSelect={handleFeedSelect} onSelect={handleFeedSelect}
emptyText="Enter a keyword and search" emptyText="输入关键词进行搜索"
/> />
</div> </div>
)} )}
@@ -178,11 +178,11 @@ export function BrowserPage() {
<div className="space-y-4"> <div className="space-y-4">
{!userView && ( {!userView && (
<div className="text-center py-12 text-dark-muted"> <div className="text-center py-12 text-dark-muted">
<p>Click on a user in a feed detail to view their profile</p> <p></p>
<p className="text-xs mt-2">Or enter user details manually:</p> <p className="text-xs mt-2"></p>
<div className="flex gap-3 items-end justify-center mt-4"> <div className="flex gap-3 items-end justify-center mt-4">
<Input <Input
placeholder="User ID" placeholder="用户 ID"
value={manualUserId} value={manualUserId}
onChange={(e) => setManualUserId(e.target.value)} onChange={(e) => setManualUserId(e.target.value)}
/> />
@@ -199,7 +199,7 @@ export function BrowserPage() {
} }
}} }}
> >
Load
</Button> </Button>
</div> </div>
</div> </div>
@@ -207,7 +207,7 @@ export function BrowserPage() {
{userView && userView.userId && userView.xsecToken && ( {userView && userView.userId && userView.xsecToken && (
<div> <div>
<Button variant="ghost" size="sm" onClick={() => setUserView(null)} className="mb-4"> <Button variant="ghost" size="sm" onClick={() => setUserView(null)} className="mb-4">
&larr; Back &larr;
</Button> </Button>
<UserCard <UserCard
userId={userView.userId} userId={userView.userId}
+19 -19
View File
@@ -15,9 +15,9 @@ export function DashboardPage() {
return ( return (
<div className="space-y-6 max-w-5xl"> <div className="space-y-6 max-w-5xl">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Dashboard</h1> <h1 className="text-2xl font-bold"></h1>
<Button variant="ghost" size="sm" onClick={() => void refreshHealth()}> <Button variant="ghost" size="sm" onClick={() => void refreshHealth()}>
Refresh
</Button> </Button>
</div> </div>
@@ -25,24 +25,24 @@ export function DashboardPage() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Server Status */} {/* Server Status */}
<Card> <Card>
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2">Server</div> <div className="text-xs text-dark-muted uppercase tracking-wider mb-2"></div>
{healthLoading ? ( {healthLoading ? (
<Spinner size="sm" /> <Spinner size="sm" />
) : health ? ( ) : health ? (
<div className="space-y-1"> <div className="space-y-1">
<Badge variant={health.healthy ? 'success' : 'danger'}> <Badge variant={health.healthy ? 'success' : 'danger'}>
{health.healthy ? 'Healthy' : 'Unhealthy'} {health.healthy ? '正常' : '异常'}
</Badge> </Badge>
<p className="text-sm text-dark-muted">v{health.version}</p> <p className="text-sm text-dark-muted">v{health.version}</p>
</div> </div>
) : ( ) : (
<Badge variant="danger">Offline</Badge> <Badge variant="danger">线</Badge>
)} )}
</Card> </Card>
{/* Uptime */} {/* Uptime */}
<Card> <Card>
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2">Uptime</div> <div className="text-xs text-dark-muted uppercase tracking-wider mb-2"></div>
{health ? ( {health ? (
<p className="text-xl font-mono font-bold text-dark-text">{formatUptime(health.uptime)}</p> <p className="text-xl font-mono font-bold text-dark-text">{formatUptime(health.uptime)}</p>
) : ( ) : (
@@ -52,13 +52,13 @@ export function DashboardPage() {
{/* Login Status */} {/* Login Status */}
<Card> <Card>
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2">Xiaohongshu Login</div> <div className="text-xs text-dark-muted uppercase tracking-wider mb-2"></div>
{loginLoading ? ( {loginLoading ? (
<Spinner size="sm" /> <Spinner size="sm" />
) : loginStatus ? ( ) : loginStatus ? (
<div className="space-y-1"> <div className="space-y-1">
<Badge variant={loginStatus.loggedIn ? 'success' : 'warning'}> <Badge variant={loginStatus.loggedIn ? 'success' : 'warning'}>
{loginStatus.loggedIn ? 'Logged In' : 'Not Logged In'} {loginStatus.loggedIn ? '已登录' : '未登录'}
</Badge> </Badge>
{loginStatus.username && ( {loginStatus.username && (
<p className="text-sm text-dark-muted">{loginStatus.username}</p> <p className="text-sm text-dark-muted">{loginStatus.username}</p>
@@ -69,18 +69,18 @@ export function DashboardPage() {
onClick={() => void refreshLogin()} onClick={() => void refreshLogin()}
className="text-xs text-dark-accent hover:underline" className="text-xs text-dark-accent hover:underline"
> >
Click to check
</button> </button>
)} )}
</Card> </Card>
{/* Memory */} {/* Memory */}
<Card> <Card>
<div className="text-xs text-dark-muted uppercase tracking-wider mb-2">Memory</div> <div className="text-xs text-dark-muted uppercase tracking-wider mb-2"></div>
{health ? ( {health ? (
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xl font-mono font-bold text-dark-text">{health.memory.heapUsed} MB</p> <p className="text-xl font-mono font-bold text-dark-text">{health.memory.heapUsed} MB</p>
<p className="text-xs text-dark-muted">of {health.memory.heapTotal} MB heap</p> <p className="text-xs text-dark-muted"> {health.memory.heapTotal} MB </p>
</div> </div>
) : ( ) : (
<p className="text-dark-muted">-</p> <p className="text-dark-muted">-</p>
@@ -91,13 +91,13 @@ export function DashboardPage() {
{/* Plugin Health */} {/* Plugin Health */}
{health && Object.keys(health.plugins).length > 0 && ( {health && Object.keys(health.plugins).length > 0 && (
<Card> <Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">Plugins</h2> <h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3"></h2>
<div className="space-y-2"> <div className="space-y-2">
{Object.entries(health.plugins).map(([name, info]) => ( {Object.entries(health.plugins).map(([name, info]) => (
<div key={name} className="flex items-center justify-between py-1"> <div key={name} className="flex items-center justify-between py-1">
<span className="text-sm">{name}</span> <span className="text-sm">{name}</span>
<Badge variant={info.healthy ? 'success' : 'danger'}> <Badge variant={info.healthy ? 'success' : 'danger'}>
{info.healthy ? 'Healthy' : info.message || 'Unhealthy'} {info.healthy ? '正常' : info.message || '异常'}
</Badge> </Badge>
</div> </div>
))} ))}
@@ -107,19 +107,19 @@ export function DashboardPage() {
{/* Quick actions */} {/* Quick actions */}
<Card> <Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">Quick Actions</h2> <h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3"></h2>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<Button variant="secondary" size="sm" onClick={() => navigate('/login')}> <Button variant="secondary" size="sm" onClick={() => navigate('/login')}>
Manage Login
</Button> </Button>
<Button variant="secondary" size="sm" onClick={() => navigate('/browser')}> <Button variant="secondary" size="sm" onClick={() => navigate('/browser')}>
Browse Content
</Button> </Button>
<Button variant="secondary" size="sm" onClick={() => navigate('/publish')}> <Button variant="secondary" size="sm" onClick={() => navigate('/publish')}>
Publish Note
</Button> </Button>
<Button variant="secondary" size="sm" onClick={() => navigate('/api-tester')}> <Button variant="secondary" size="sm" onClick={() => navigate('/api-tester')}>
API Tester API
</Button> </Button>
</div> </div>
</Card> </Card>
@@ -128,7 +128,7 @@ export function DashboardPage() {
{health && ( {health && (
<Card> <Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3"> <h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">
Raw Health Data
</h2> </h2>
<pre className="bg-dark-bg border border-dark-border rounded-lg p-4 text-xs text-dark-text overflow-auto font-mono max-h-64"> <pre className="bg-dark-bg border border-dark-border rounded-lg p-4 text-xs text-dark-text overflow-auto font-mono max-h-64">
{JSON.stringify(health, null, 2)} {JSON.stringify(health, null, 2)}
+36 -36
View File
@@ -37,7 +37,7 @@ export function InteractionsPage() {
const checkIds = () => { const checkIds = () => {
if (!feedId.trim() || !xsecToken.trim()) { if (!feedId.trim() || !xsecToken.trim()) {
toast('warning', 'Feed ID and xsec_token are required'); toast('warning', 'Feed ID xsec_token 为必填项');
return false; return false;
} }
return true; return true;
@@ -48,11 +48,11 @@ export function InteractionsPage() {
setLoading(unlike ? 'unlike' : 'like'); setLoading(unlike ? 'unlike' : 'like');
try { try {
const res = await toggleLike(feedId, xsecToken, unlike); const res = await toggleLike(feedId, xsecToken, unlike);
addLog(unlike ? 'Unlike' : 'Like', res); addLog(unlike ? '取消点赞' : '点赞', res);
toast('success', unlike ? 'Unliked' : 'Liked'); toast('success', unlike ? '已取消点赞' : '已点赞');
} catch (err) { } catch (err) {
const msg = err instanceof Error ? err.message : 'Failed'; const msg = err instanceof Error ? err.message : '操作失败';
addLog(unlike ? 'Unlike' : 'Like', { error: msg }); addLog(unlike ? '取消点赞' : '点赞', { error: msg });
toast('error', msg); toast('error', msg);
} finally { } finally {
setLoading(null); setLoading(null);
@@ -64,11 +64,11 @@ export function InteractionsPage() {
setLoading(unfavorite ? 'unfavorite' : 'favorite'); setLoading(unfavorite ? 'unfavorite' : 'favorite');
try { try {
const res = await toggleFavorite(feedId, xsecToken, unfavorite); const res = await toggleFavorite(feedId, xsecToken, unfavorite);
addLog(unfavorite ? 'Unfavorite' : 'Favorite', res); addLog(unfavorite ? '取消收藏' : '收藏', res);
toast('success', unfavorite ? 'Unfavorited' : 'Favorited'); toast('success', unfavorite ? '已取消收藏' : '已收藏');
} catch (err) { } catch (err) {
const msg = err instanceof Error ? err.message : 'Failed'; const msg = err instanceof Error ? err.message : '操作失败';
addLog(unfavorite ? 'Unfavorite' : 'Favorite', { error: msg }); addLog(unfavorite ? '取消收藏' : '收藏', { error: msg });
toast('error', msg); toast('error', msg);
} finally { } finally {
setLoading(null); setLoading(null);
@@ -77,18 +77,18 @@ export function InteractionsPage() {
const handleComment = useCallback(async () => { const handleComment = useCallback(async () => {
if (!checkIds() || !commentText.trim()) { if (!checkIds() || !commentText.trim()) {
toast('warning', 'Comment text is required'); toast('warning', '评论内容为必填项');
return; return;
} }
setLoading('comment'); setLoading('comment');
try { try {
const res = await postComment(feedId, xsecToken, commentText); const res = await postComment(feedId, xsecToken, commentText);
addLog('Comment', res); addLog('评论', res);
toast('success', 'Comment posted'); toast('success', '评论已发布');
setCommentText(''); setCommentText('');
} catch (err) { } catch (err) {
const msg = err instanceof Error ? err.message : 'Failed'; const msg = err instanceof Error ? err.message : '操作失败';
addLog('Comment', { error: msg }); addLog('评论', { error: msg });
toast('error', msg); toast('error', msg);
} finally { } finally {
setLoading(null); setLoading(null);
@@ -97,7 +97,7 @@ export function InteractionsPage() {
const handleReply = useCallback(async () => { const handleReply = useCallback(async () => {
if (!checkIds() || !replyText.trim()) { if (!checkIds() || !replyText.trim()) {
toast('warning', 'Reply text is required'); toast('warning', '回复内容为必填项');
return; return;
} }
setLoading('reply'); setLoading('reply');
@@ -109,12 +109,12 @@ export function InteractionsPage() {
comment_id: replyCommentId || undefined, comment_id: replyCommentId || undefined,
user_id: replyUserId || undefined, user_id: replyUserId || undefined,
}); });
addLog('Reply', res); addLog('回复', res);
toast('success', 'Reply posted'); toast('success', '回复已发布');
setReplyText(''); setReplyText('');
} catch (err) { } catch (err) {
const msg = err instanceof Error ? err.message : 'Failed'; const msg = err instanceof Error ? err.message : '操作失败';
addLog('Reply', { error: msg }); addLog('回复', { error: msg });
toast('error', msg); toast('error', msg);
} finally { } finally {
setLoading(null); setLoading(null);
@@ -123,11 +123,11 @@ export function InteractionsPage() {
return ( return (
<div className="max-w-3xl space-y-6"> <div className="max-w-3xl space-y-6">
<h1 className="text-2xl font-bold">Interactions</h1> <h1 className="text-2xl font-bold"></h1>
{/* Target */} {/* Target */}
<Card> <Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">Target Note</h2> <h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3"></h2>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Input label="Feed ID" value={feedId} onChange={(e) => setFeedId(e.target.value)} placeholder="Feed ID" /> <Input label="Feed ID" value={feedId} onChange={(e) => setFeedId(e.target.value)} placeholder="Feed ID" />
<Input label="xsec_token" value={xsecToken} onChange={(e) => setXsecToken(e.target.value)} placeholder="xsec_token" /> <Input label="xsec_token" value={xsecToken} onChange={(e) => setXsecToken(e.target.value)} placeholder="xsec_token" />
@@ -136,45 +136,45 @@ export function InteractionsPage() {
{/* Like / Favorite */} {/* Like / Favorite */}
<Card> <Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">Quick Actions</h2> <h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3"></h2>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<Button onClick={() => void handleLike(false)} loading={loading === 'like'} size="sm"> <Button onClick={() => void handleLike(false)} loading={loading === 'like'} size="sm">
Like
</Button> </Button>
<Button onClick={() => void handleLike(true)} loading={loading === 'unlike'} variant="secondary" size="sm"> <Button onClick={() => void handleLike(true)} loading={loading === 'unlike'} variant="secondary" size="sm">
Unlike
</Button> </Button>
<Button onClick={() => void handleFavorite(false)} loading={loading === 'favorite'} size="sm"> <Button onClick={() => void handleFavorite(false)} loading={loading === 'favorite'} size="sm">
Favorite
</Button> </Button>
<Button onClick={() => void handleFavorite(true)} loading={loading === 'unfavorite'} variant="secondary" size="sm"> <Button onClick={() => void handleFavorite(true)} loading={loading === 'unfavorite'} variant="secondary" size="sm">
Unfavorite
</Button> </Button>
</div> </div>
</Card> </Card>
{/* Comment */} {/* Comment */}
<Card> <Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">Post Comment</h2> <h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3"></h2>
<div className="space-y-3"> <div className="space-y-3">
<Textarea value={commentText} onChange={(e) => setCommentText(e.target.value)} placeholder="Write a comment..." /> <Textarea value={commentText} onChange={(e) => setCommentText(e.target.value)} placeholder="写评论..." />
<Button onClick={() => void handleComment()} loading={loading === 'comment'} size="sm"> <Button onClick={() => void handleComment()} loading={loading === 'comment'} size="sm">
Post Comment
</Button> </Button>
</div> </div>
</Card> </Card>
{/* Reply */} {/* Reply */}
<Card> <Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">Reply to Comment</h2> <h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3"></h2>
<div className="space-y-3"> <div className="space-y-3">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Input label="Comment ID" value={replyCommentId} onChange={(e) => setReplyCommentId(e.target.value)} placeholder="Optional" /> <Input label="评论 ID" value={replyCommentId} onChange={(e) => setReplyCommentId(e.target.value)} placeholder="可选" />
<Input label="User ID" value={replyUserId} onChange={(e) => setReplyUserId(e.target.value)} placeholder="Optional" /> <Input label="用户 ID" value={replyUserId} onChange={(e) => setReplyUserId(e.target.value)} placeholder="可选" />
</div> </div>
<Textarea value={replyText} onChange={(e) => setReplyText(e.target.value)} placeholder="Write a reply..." /> <Textarea value={replyText} onChange={(e) => setReplyText(e.target.value)} placeholder="写回复..." />
<Button onClick={() => void handleReply()} loading={loading === 'reply'} size="sm"> <Button onClick={() => void handleReply()} loading={loading === 'reply'} size="sm">
Post Reply
</Button> </Button>
</div> </div>
</Card> </Card>
@@ -183,8 +183,8 @@ export function InteractionsPage() {
{log.length > 0 && ( {log.length > 0 && (
<Card> <Card>
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider">Action Log</h2> <h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider"></h2>
<Button variant="ghost" size="sm" onClick={() => setLog([])}>Clear</Button> <Button variant="ghost" size="sm" onClick={() => setLog([])}></Button>
</div> </div>
<div className="space-y-3 max-h-96 overflow-y-auto"> <div className="space-y-3 max-h-96 overflow-y-auto">
{log.map((entry) => ( {log.map((entry) => (
+25 -25
View File
@@ -48,7 +48,7 @@ export function LoginPage() {
const res = await getLoginQRCode(); const res = await getLoginQRCode();
if (res.success && res.data) { if (res.success && res.data) {
if (res.data.alreadyLoggedIn) { if (res.data.alreadyLoggedIn) {
toast('info', 'Already logged in!'); toast('info', '已经登录!');
void refreshStatus(); void refreshStatus();
return; return;
} }
@@ -62,7 +62,7 @@ export function LoginPage() {
if (cookieRes.success && cookieRes.data?.hasCookies) { if (cookieRes.success && cookieRes.data?.hasCookies) {
stopPolling(); stopPolling();
setQrData(null); setQrData(null);
toast('success', 'Login successful!'); toast('success', '登录成功!');
void refreshStatus(); void refreshStatus();
} }
} catch { } catch {
@@ -74,17 +74,17 @@ export function LoginPage() {
if (prev <= 1) { if (prev <= 1) {
stopPolling(); stopPolling();
setQrData(null); setQrData(null);
toast('warning', 'QR code expired'); toast('warning', '二维码已过期');
return 0; return 0;
} }
return prev - 1; return prev - 1;
}); });
}, 1000); }, 1000);
} else { } else {
toast('error', res.error?.message || 'Failed to get QR code'); toast('error', res.error?.message || '获取二维码失败');
} }
} catch (err) { } catch (err) {
toast('error', err instanceof Error ? err.message : 'Failed to get QR code'); toast('error', err instanceof Error ? err.message : '获取二维码失败');
} finally { } finally {
setQrLoading(false); setQrLoading(false);
} }
@@ -95,13 +95,13 @@ export function LoginPage() {
try { try {
const res = await deleteCookies(); const res = await deleteCookies();
if (res.success) { if (res.success) {
toast('success', 'Logged out successfully'); toast('success', '已成功登出');
void refreshStatus(); void refreshStatus();
} else { } else {
toast('error', res.error?.message || 'Failed to logout'); toast('error', res.error?.message || '登出失败');
} }
} catch (err) { } catch (err) {
toast('error', err instanceof Error ? err.message : 'Failed to logout'); toast('error', err instanceof Error ? err.message : '登出失败');
} finally { } finally {
setLogoutLoading(false); setLogoutLoading(false);
} }
@@ -119,16 +119,16 @@ export function LoginPage() {
return ( return (
<div className="max-w-2xl space-y-6"> <div className="max-w-2xl space-y-6">
<h1 className="text-2xl font-bold">Xiaohongshu Login</h1> <h1 className="text-2xl font-bold"></h1>
{!token && ( {!token && (
<Card className="border-dark-warning/30 bg-dark-warning/10"> <Card className="border-dark-warning/30 bg-dark-warning/10">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-sm text-dark-warning"> <p className="text-sm text-dark-warning">
Bearer Token API 401 Settings Token Bearer Token API 401 Token
</p> </p>
<Button size="sm" variant="secondary" onClick={() => navigate('/settings')}> <Button size="sm" variant="secondary" onClick={() => navigate('/settings')}>
Settings
</Button> </Button>
</div> </div>
</Card> </Card>
@@ -139,28 +139,28 @@ export function LoginPage() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-2"> <h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-2">
Current Status
</h2> </h2>
{statusLoading ? ( {statusLoading ? (
<Spinner size="sm" /> <Spinner size="sm" />
) : status ? ( ) : status ? (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Badge variant={status.loggedIn ? 'success' : 'warning'}> <Badge variant={status.loggedIn ? 'success' : 'warning'}>
{status.loggedIn ? 'Logged In' : 'Not Logged In'} {status.loggedIn ? '已登录' : '未登录'}
</Badge> </Badge>
{status.username && <span className="text-sm">{status.username}</span>} {status.username && <span className="text-sm">{status.username}</span>}
</div> </div>
) : ( ) : (
<span className="text-sm text-dark-muted">Click Refresh to check login status</span> <span className="text-sm text-dark-muted"></span>
)} )}
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={() => void refreshStatus()}> <Button variant="ghost" size="sm" onClick={() => void refreshStatus()}>
Refresh
</Button> </Button>
{status?.loggedIn && ( {status?.loggedIn && (
<Button variant="danger" size="sm" onClick={() => void handleLogout()} loading={logoutLoading}> <Button variant="danger" size="sm" onClick={() => void handleLogout()} loading={logoutLoading}>
Logout
</Button> </Button>
)} )}
</div> </div>
@@ -170,14 +170,14 @@ export function LoginPage() {
{/* QR Code Login */} {/* QR Code Login */}
<Card> <Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4"> <h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4">
QR Code Login
</h2> </h2>
{!qrData && !qrLoading && ( {!qrData && !qrLoading && (
<div className="text-center py-8"> <div className="text-center py-8">
<p className="text-dark-muted mb-4">Click the button to generate a QR code for login</p> <p className="text-dark-muted mb-4"></p>
<Button onClick={() => void handleGetQR()} disabled={status?.loggedIn}> <Button onClick={() => void handleGetQR()} disabled={status?.loggedIn}>
Get QR Code
</Button> </Button>
</div> </div>
)} )}
@@ -185,30 +185,30 @@ export function LoginPage() {
{qrLoading && ( {qrLoading && (
<div className="flex flex-col items-center py-8 gap-3"> <div className="flex flex-col items-center py-8 gap-3">
<Spinner size="lg" /> <Spinner size="lg" />
<p className="text-sm text-dark-muted">Generating QR code...</p> <p className="text-sm text-dark-muted">...</p>
</div> </div>
)} )}
{qrData && ( {qrData && (
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col items-center gap-4">
<div className="bg-white rounded-xl p-4"> <div className="bg-white rounded-xl p-4">
<img src={qrData} alt="Login QR Code" className="w-64 h-64" /> <img src={qrData} alt="登录二维码" className="w-64 h-64" />
</div> </div>
<p className="text-sm text-dark-muted">Scan with Xiaohongshu app to login</p> <p className="text-sm text-dark-muted">使 App </p>
{polling && ( {polling && (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Spinner size="sm" /> <Spinner size="sm" />
<span className="text-sm text-dark-accent"> <span className="text-sm text-dark-accent">
Waiting for scan... {formatCountdown(countdown)} ... {formatCountdown(countdown)}
</span> </span>
</div> </div>
)} )}
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={() => void handleGetQR()}> <Button variant="ghost" size="sm" onClick={() => void handleGetQR()}>
Refresh QR
</Button> </Button>
<Button variant="ghost" size="sm" onClick={stopPolling}> <Button variant="ghost" size="sm" onClick={stopPolling}>
Cancel
</Button> </Button>
</div> </div>
</div> </div>
+30 -30
View File
@@ -34,7 +34,7 @@ export function PublishPage() {
const handlePublishImage = useCallback(async () => { const handlePublishImage = useCallback(async () => {
if (!imgTitle.trim() || !imgPaths.trim()) { if (!imgTitle.trim() || !imgPaths.trim()) {
toast('warning', 'Title and images are required'); toast('warning', '标题和图片为必填项');
return; return;
} }
setImgLoading(true); setImgLoading(true);
@@ -52,12 +52,12 @@ export function PublishPage() {
}); });
setImgResult(res); setImgResult(res);
if (res.success) { if (res.success) {
toast('success', 'Image note published!'); toast('success', '图文笔记发布成功!');
} else { } else {
toast('error', res.error?.message || 'Publish failed'); toast('error', res.error?.message || '发布失败');
} }
} catch (err) { } catch (err) {
const msg = err instanceof Error ? err.message : 'Publish failed'; const msg = err instanceof Error ? err.message : '发布失败';
toast('error', msg); toast('error', msg);
setImgResult({ error: msg }); setImgResult({ error: msg });
} finally { } finally {
@@ -67,7 +67,7 @@ export function PublishPage() {
const handlePublishVideo = useCallback(async () => { const handlePublishVideo = useCallback(async () => {
if (!vidTitle.trim() || !vidPath.trim()) { if (!vidTitle.trim() || !vidPath.trim()) {
toast('warning', 'Title and video path are required'); toast('warning', '标题和视频路径为必填项');
return; return;
} }
setVidLoading(true); setVidLoading(true);
@@ -83,12 +83,12 @@ export function PublishPage() {
}); });
setVidResult(res); setVidResult(res);
if (res.success) { if (res.success) {
toast('success', 'Video note published!'); toast('success', '视频笔记发布成功!');
} else { } else {
toast('error', res.error?.message || 'Publish failed'); toast('error', res.error?.message || '发布失败');
} }
} catch (err) { } catch (err) {
const msg = err instanceof Error ? err.message : 'Publish failed'; const msg = err instanceof Error ? err.message : '发布失败';
toast('error', msg); toast('error', msg);
setVidResult({ error: msg }); setVidResult({ error: msg });
} finally { } finally {
@@ -98,12 +98,12 @@ export function PublishPage() {
return ( return (
<div className="max-w-3xl space-y-4"> <div className="max-w-3xl space-y-4">
<h1 className="text-2xl font-bold">Publish Note</h1> <h1 className="text-2xl font-bold"></h1>
<Tabs <Tabs
tabs={[ tabs={[
{ key: 'image', label: 'Image Note' }, { key: 'image', label: '图文笔记' },
{ key: 'video', label: 'Video Note' }, { key: 'video', label: '视频笔记' },
]} ]}
active={tab} active={tab}
onChange={setTab} onChange={setTab}
@@ -112,28 +112,28 @@ export function PublishPage() {
{tab === 'image' && ( {tab === 'image' && (
<Card> <Card>
<div className="space-y-4"> <div className="space-y-4">
<Input label="Title" value={imgTitle} onChange={(e) => setImgTitle(e.target.value)} placeholder="Note title" /> <Input label="标题" value={imgTitle} onChange={(e) => setImgTitle(e.target.value)} placeholder="笔记标题" />
<Textarea label="Content" value={imgContent} onChange={(e) => setImgContent(e.target.value)} placeholder="Note body text" /> <Textarea label="正文" value={imgContent} onChange={(e) => setImgContent(e.target.value)} placeholder="笔记正文" />
<Textarea label="Image Paths (one per line)" value={imgPaths} onChange={(e) => setImgPaths(e.target.value)} placeholder="/path/to/image1.jpg&#10;/path/to/image2.jpg" /> <Textarea label="图片路径(每行一个)" value={imgPaths} onChange={(e) => setImgPaths(e.target.value)} placeholder="/path/to/image1.jpg&#10;/path/to/image2.jpg" />
<Input label="Tags (comma separated)" value={imgTags} onChange={(e) => setImgTags(e.target.value)} placeholder="travel, food" /> <Input label="标签(逗号分隔)" value={imgTags} onChange={(e) => setImgTags(e.target.value)} placeholder="旅行, 美食" />
<div className="flex gap-4 items-end"> <div className="flex gap-4 items-end">
<Select <Select
label="Visibility" label="可见性"
options={[ options={[
{ value: 'public', label: 'Public' }, { value: 'public', label: '公开' },
{ value: 'private', label: 'Private' }, { value: 'private', label: '私密' },
{ value: 'friends', label: 'Friends' }, { value: 'friends', label: '仅好友' },
]} ]}
value={imgVisibility} value={imgVisibility}
onChange={(e) => setImgVisibility(e.target.value)} onChange={(e) => setImgVisibility(e.target.value)}
/> />
<label className="flex items-center gap-2 pb-2 cursor-pointer"> <label className="flex items-center gap-2 pb-2 cursor-pointer">
<input type="checkbox" checked={imgOriginal} onChange={(e) => setImgOriginal(e.target.checked)} className="rounded" /> <input type="checkbox" checked={imgOriginal} onChange={(e) => setImgOriginal(e.target.checked)} className="rounded" />
<span className="text-sm text-dark-muted">Original content</span> <span className="text-sm text-dark-muted"></span>
</label> </label>
</div> </div>
<Button onClick={() => void handlePublishImage()} loading={imgLoading}> <Button onClick={() => void handlePublishImage()} loading={imgLoading}>
Publish Image Note
</Button> </Button>
{imgResult !== null && <JsonViewer data={imgResult} />} {imgResult !== null && <JsonViewer data={imgResult} />}
</div> </div>
@@ -143,22 +143,22 @@ export function PublishPage() {
{tab === 'video' && ( {tab === 'video' && (
<Card> <Card>
<div className="space-y-4"> <div className="space-y-4">
<Input label="Title" value={vidTitle} onChange={(e) => setVidTitle(e.target.value)} placeholder="Note title" /> <Input label="标题" value={vidTitle} onChange={(e) => setVidTitle(e.target.value)} placeholder="笔记标题" />
<Textarea label="Content" value={vidContent} onChange={(e) => setVidContent(e.target.value)} placeholder="Note body text" /> <Textarea label="正文" value={vidContent} onChange={(e) => setVidContent(e.target.value)} placeholder="笔记正文" />
<Input label="Video Path" value={vidPath} onChange={(e) => setVidPath(e.target.value)} placeholder="/path/to/video.mp4" /> <Input label="视频路径" value={vidPath} onChange={(e) => setVidPath(e.target.value)} placeholder="/path/to/video.mp4" />
<Input label="Tags (comma separated)" value={vidTags} onChange={(e) => setVidTags(e.target.value)} placeholder="travel, vlog" /> <Input label="标签(逗号分隔)" value={vidTags} onChange={(e) => setVidTags(e.target.value)} placeholder="旅行, vlog" />
<Select <Select
label="Visibility" label="可见性"
options={[ options={[
{ value: 'public', label: 'Public' }, { value: 'public', label: '公开' },
{ value: 'private', label: 'Private' }, { value: 'private', label: '私密' },
{ value: 'friends', label: 'Friends' }, { value: 'friends', label: '仅好友' },
]} ]}
value={vidVisibility} value={vidVisibility}
onChange={(e) => setVidVisibility(e.target.value)} onChange={(e) => setVidVisibility(e.target.value)}
/> />
<Button onClick={() => void handlePublishVideo()} loading={vidLoading}> <Button onClick={() => void handlePublishVideo()} loading={vidLoading}>
Publish Video Note
</Button> </Button>
{vidResult !== null && <JsonViewer data={vidResult} />} {vidResult !== null && <JsonViewer data={vidResult} />}
</div> </div>
+18 -18
View File
@@ -10,49 +10,49 @@ export function SettingsPage() {
return ( return (
<div className="max-w-2xl space-y-6"> <div className="max-w-2xl space-y-6">
<h1 className="text-2xl font-bold">Settings</h1> <h1 className="text-2xl font-bold"></h1>
{/* Server Connection */} {/* Server Connection */}
<Card> <Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4">Server Connection</h2> <h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4"></h2>
<div className="space-y-4"> <div className="space-y-4">
<Input <Input
label="Server URL" label="服务器地址"
value={serverUrl} value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)} onChange={(e) => setServerUrl(e.target.value)}
placeholder="Leave empty for same-origin (default)" placeholder="留空则使用同源地址(默认)"
/> />
<p className="text-xs text-dark-muted"> <p className="text-xs text-dark-muted">
Leave empty when the dashboard is served by the same Express server. Dashboard Express {' '}
Set to e.g. <code className="text-dark-accent">http://192.168.1.100:3000</code> for remote servers. <code className="text-dark-accent">http://192.168.1.100:3000</code> 用于远程服务器。
</p> </p>
</div> </div>
</Card> </Card>
{/* Authentication */} {/* Authentication */}
<Card> <Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4">Authentication</h2> <h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4"></h2>
<div className="space-y-4"> <div className="space-y-4">
<Input <Input
label="Bearer Token" label="Bearer Token"
type="password" type="password"
value={token} value={token}
onChange={(e) => setToken(e.target.value)} onChange={(e) => setToken(e.target.value)}
placeholder="Enter your API token" placeholder="输入 API Token"
/> />
<p className="text-xs text-dark-muted"> <p className="text-xs text-dark-muted">
Token from <code className="text-dark-accent">BEARER_TOKEN</code> environment variable or{' '} Token <code className="text-dark-accent">BEARER_TOKEN</code> {' '}
<code className="text-dark-accent">.social-mcp/bearer-token</code> file. <code className="text-dark-accent">.social-mcp/bearer-token</code>
</p> </p>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
variant="secondary" variant="secondary"
size="sm" size="sm"
onClick={() => { onClick={() => {
toast('success', 'Settings saved'); toast('success', '设置已保存');
}} }}
> >
Save
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
@@ -60,10 +60,10 @@ export function SettingsPage() {
onClick={() => { onClick={() => {
setToken(''); setToken('');
setServerUrl(''); setServerUrl('');
toast('info', 'Settings cleared'); toast('info', '设置已清除');
}} }}
> >
Clear All
</Button> </Button>
</div> </div>
</div> </div>
@@ -71,11 +71,11 @@ export function SettingsPage() {
{/* About */} {/* About */}
<Card> <Card>
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4">About</h2> <h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-4"></h2>
<div className="space-y-2 text-sm text-dark-muted"> <div className="space-y-2 text-sm text-dark-muted">
<p><span className="text-dark-text">Social MCP</span> Multi-platform social media automation</p> <p><span className="text-dark-text">Social MCP</span> </p>
<p>Version: 0.1.0</p> <p>0.1.0</p>
<p>Stack: React 19 + TypeScript + Tailwind CSS</p> <p>React 19 + TypeScript + Tailwind CSS</p>
</div> </div>
</Card> </Card>
</div> </div>