i18n: translate frontend UI strings to English

This commit is contained in:
zhi
2026-03-11 21:19:54 +00:00
parent 34ab80e50d
commit 0ab1d2f380
14 changed files with 147 additions and 147 deletions

View File

@@ -50,14 +50,14 @@ export default function App() {
}
if (appState === 'checking') {
return <div className="loading">...</div>
return <div className="loading">Checking configuration status...</div>
}
if (appState === 'setup') {
return <SetupWizardPage wizardBase={WIZARD_BASE} onComplete={checkInitialized} />
}
if (loading) return <div className="loading">...</div>
if (loading) return <div className="loading">Loading...</div>
if (!user) {
return (

View File

@@ -29,11 +29,11 @@ export default function Sidebar({ user, onLogout }: Props) {
}, [user])
const links = user ? [
{ to: '/', icon: '📊', label: '仪表盘' },
{ to: '/', icon: '📊', label: 'Dashboard' },
{ to: '/issues', icon: '📋', label: 'Issues' },
{ to: '/projects', icon: '📁', label: '项目' },
{ to: '/milestones', icon: '🏁', label: '里程碑' },
{ to: '/notifications', icon: '🔔', label: '通知' + (unreadCount > 0 ? ' (' + unreadCount + ')' : '') },
{ to: '/projects', icon: '📁', label: 'Projects' },
{ to: '/milestones', icon: '🏁', label: 'Milestones' },
{ to: '/notifications', icon: '🔔', label: 'Notifications' + (unreadCount > 0 ? ' (' + unreadCount + ')' : '') },
{ to: '/monitor', icon: '📡', label: 'Monitor' },
] : [
{ to: '/monitor', icon: '📡', label: 'Monitor' },
@@ -54,7 +54,7 @@ export default function Sidebar({ user, onLogout }: Props) {
{user && (
<div className="sidebar-footer">
<span>👤 {user.username}</span>
<button onClick={onLogout}>退</button>
<button onClick={onLogout}>Logout</button>
</div>
)}
</nav>

View File

@@ -27,16 +27,16 @@ export default function CreateIssuePage() {
return (
<div className="create-issue">
<h2> Issue</h2>
<h2>Create Issue</h2>
<form onSubmit={submit}>
<label> <input required value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} /></label>
<label> <textarea value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} /></label>
<label>
<label>Title <input required value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} /></label>
<label>Description <textarea value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} /></label>
<label>Projects
<select value={form.project_id} onChange={(e) => setForm({ ...form, project_id: Number(e.target.value) })}>
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
</label>
<label>
<label>Type
<select value={form.issue_type} onChange={(e) => setForm({ ...form, issue_type: e.target.value })}>
<option value="task">Task</option>
<option value="bug">Bug</option>
@@ -44,7 +44,7 @@ export default function CreateIssuePage() {
<option value="resolution">Resolution</option>
</select>
</label>
<label>
<label>Priority
<select value={form.priority} onChange={(e) => setForm({ ...form, priority: e.target.value })}>
<option value="low">Low</option>
<option value="medium">Medium</option>
@@ -52,8 +52,8 @@ export default function CreateIssuePage() {
<option value="critical">Critical</option>
</select>
</label>
<label> <input value={form.tags} onChange={(e) => setForm({ ...form, tags: e.target.value })} placeholder="逗号分隔" /></label>
<button type="submit" className="btn-primary"></button>
<label>Tags <input value={form.tags} onChange={(e) => setForm({ ...form, tags: e.target.value })} placeholder="Comma separated" /></label>
<button type="submit" className="btn-primary">Create</button>
</form>
</div>
)

View File

@@ -9,7 +9,7 @@ export default function DashboardPage() {
api.get<DashboardStats>('/dashboard/stats').then(({ data }) => setStats(data))
}, [])
if (!stats) return <div className="loading">...</div>
if (!stats) return <div className="loading">Loading...</div>
const statusColors: Record<string, string> = {
open: '#3b82f6', in_progress: '#f59e0b', resolved: '#10b981',
@@ -21,12 +21,12 @@ export default function DashboardPage() {
return (
<div className="dashboard">
<h2>📊 </h2>
<h2>📊 Dashboard</h2>
<div className="stats-grid">
<div className="stat-card total">
<span className="stat-number">{stats.total_issues}</span>
<span className="stat-label"> Issues</span>
<span className="stat-label">Total Issues</span>
</div>
{Object.entries(stats.by_status || {}).map(([k, v]) => (
<div className="stat-card" key={k} style={{ borderLeftColor: statusColors[k] || '#ccc' }}>
@@ -37,7 +37,7 @@ export default function DashboardPage() {
</div>
<div className="section">
<h3></h3>
<h3>By Priority</h3>
<div className="bar-chart">
{Object.entries(stats.by_priority || {}).map(([k, v]) => (
<div className="bar-row" key={k}>
@@ -52,10 +52,10 @@ export default function DashboardPage() {
</div>
<div className="section">
<h3> Issues</h3>
<h3>Recent Issues</h3>
<table>
<thead>
<tr><th>ID</th><th></th><th></th><th></th><th></th></tr>
<tr><th>ID</th><th>Title</th><th>Status</th><th>Priority</th><th>Type</th></tr>
</thead>
<tbody>
{(stats.recent_issues || []).map((i) => (

View File

@@ -30,7 +30,7 @@ export default function IssueDetailPage() {
setIssue(data)
}
if (!issue) return <div className="loading">...</div>
if (!issue) return <div className="loading">Loading...</div>
const statusActions: Record<string, string[]> = {
open: ['in_progress', 'blocked'],
@@ -42,7 +42,7 @@ export default function IssueDetailPage() {
return (
<div className="issue-detail">
<button className="btn-back" onClick={() => navigate('/issues')}> </button>
<button className="btn-back" onClick={() => navigate('/issues')}> Back</button>
<div className="issue-header">
<h2>#{issue.id} {issue.title}</h2>
@@ -56,21 +56,21 @@ export default function IssueDetailPage() {
<div className="issue-body">
<div className="section">
<h3></h3>
<p>{issue.description || '暂无描述'}</p>
<h3>Description</h3>
<p>{issue.description || 'No description'}</p>
</div>
<div className="section">
<h3></h3>
<h3>Details</h3>
<dl>
<dt></dt><dd>{dayjs(issue.created_at).format('YYYY-MM-DD HH:mm')}</dd>
{issue.due_date && <><dt></dt><dd>{dayjs(issue.due_date).format('YYYY-MM-DD')}</dd></>}
{issue.updated_at && <><dt></dt><dd>{dayjs(issue.updated_at).format('YYYY-MM-DD HH:mm')}</dd></>}
<dt>Created</dt><dd>{dayjs(issue.created_at).format('YYYY-MM-DD HH:mm')}</dd>
{issue.due_date && <><dt>Due date</dt><dd>{dayjs(issue.due_date).format('YYYY-MM-DD')}</dd></>}
{issue.updated_at && <><dt>Updated</dt><dd>{dayjs(issue.updated_at).format('YYYY-MM-DD HH:mm')}</dd></>}
</dl>
</div>
<div className="section">
<h3></h3>
<h3>Status changes</h3>
<div className="actions">
{(statusActions[issue.status] || []).map((s) => (
<button key={s} className="btn-transition" onClick={() => transition(s)}>{s}</button>
@@ -79,16 +79,16 @@ export default function IssueDetailPage() {
</div>
<div className="section">
<h3> ({comments.length})</h3>
<h3>Comments ({comments.length})</h3>
{comments.map((c) => (
<div className="comment" key={c.id}>
<div className="comment-meta"> #{c.author_id} · {dayjs(c.created_at).format('MM-DD HH:mm')}</div>
<div className="comment-meta">User #{c.author_id} · {dayjs(c.created_at).format('MM-DD HH:mm')}</div>
<p>{c.content}</p>
</div>
))}
<div className="comment-form">
<textarea value={newComment} onChange={(e) => setNewComment(e.target.value)} placeholder="添加评论..." />
<button onClick={addComment} disabled={!newComment.trim()}></button>
<textarea value={newComment} onChange={(e) => setNewComment(e.target.value)} placeholder="Add a comment..." />
<button onClick={addComment} disabled={!newComment.trim()}>Submit comment</button>
</div>
</div>
</div>

View File

@@ -33,12 +33,12 @@ export default function IssuesPage() {
<div className="issues-page">
<div className="page-header">
<h2>📋 Issues ({total})</h2>
<button className="btn-primary" onClick={() => navigate('/issues/new')}>+ Issue</button>
<button className="btn-primary" onClick={() => navigate('/issues/new')}>+ Create Issue</button>
</div>
<div className="filters">
<select value={statusFilter} onChange={(e) => { setStatusFilter(e.target.value); setPage(1) }}>
<option value=""></option>
<option value="">All statuses</option>
<option value="open">Open</option>
<option value="in_progress">In Progress</option>
<option value="resolved">Resolved</option>
@@ -49,7 +49,7 @@ export default function IssuesPage() {
<table className="issues-table">
<thead>
<tr><th>#</th><th></th><th></th><th></th><th></th><th></th><th></th></tr>
<tr><th>#</th><th>Title</th><th>Status</th><th>Priority</th><th>Type</th><th>Tags</th><th>Created</th></tr>
</thead>
<tbody>
{issues.map((i) => (
@@ -68,9 +68,9 @@ export default function IssuesPage() {
{totalPages > 1 && (
<div className="pagination">
<button disabled={page <= 1} onClick={() => setPage(page - 1)}></button>
<button disabled={page <= 1} onClick={() => setPage(page - 1)}>Prev</button>
<span>{page} / {totalPages}</span>
<button disabled={page >= totalPages} onClick={() => setPage(page + 1)}></button>
<button disabled={page >= totalPages} onClick={() => setPage(page + 1)}>Next</button>
</div>
)}
</div>

View File

@@ -17,7 +17,7 @@ export default function LoginPage({ onLogin }: Props) {
try {
await onLogin(username, password)
} catch {
setError('登录失败,请检查用户名和密码')
setError('Login failed. Check username and password.')
} finally {
setLoading(false)
}
@@ -27,25 +27,25 @@ export default function LoginPage({ onLogin }: Props) {
<div className="login-page">
<div className="login-card">
<h1> HarborForge</h1>
<p className="subtitle">Agent/</p>
<p className="subtitle">Agent/Human collaborative task management platform</p>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="用户名"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
<input
type="password"
placeholder="密码"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
{error && <p className="error">{error}</p>}
<button type="submit" disabled={loading}>
{loading ? '登录中...' : '登录'}
{loading ? 'Signing in...' : 'Sign in'}
</button>
</form>
</div>

View File

@@ -17,37 +17,37 @@ export default function MilestoneDetailPage() {
api.get<Issue[]>(`/milestones/${id}/issues`).then(({ data }) => setIssues(data)).catch(() => {})
}, [id])
if (!milestone) return <div className="loading">...</div>
if (!milestone) return <div className="loading">Loading...</div>
return (
<div className="milestone-detail">
<button className="btn-back" onClick={() => navigate('/milestones')}> </button>
<button className="btn-back" onClick={() => navigate('/milestones')}> Back to milestones</button>
<div className="issue-header">
<h2>🏁 {milestone.title}</h2>
<div className="issue-meta">
<span className={`badge status-${milestone.status === 'active' ? 'open' : 'closed'}`}>{milestone.status}</span>
{milestone.due_date && <span className="text-dim"> {dayjs(milestone.due_date).format('YYYY-MM-DD')}</span>}
{milestone.due_date && <span className="text-dim">Due {dayjs(milestone.due_date).format('YYYY-MM-DD')}</span>}
</div>
</div>
{milestone.description && (
<div className="section">
<h3></h3>
<h3>Description</h3>
<p>{milestone.description}</p>
</div>
)}
{progress && (
<div className="section">
<h3></h3>
<h3>Progress</h3>
<div className="progress-bar-container">
<div className="progress-bar" style={{ width: `${progress.progress_percent}%` }}>
{progress.progress_percent.toFixed(0)}%
</div>
</div>
<p className="text-dim" style={{ marginTop: 8 }}>
{progress.completed_issues} / {progress.total_issues} issues
{progress.completed_issues} / {progress.total_issues} issues completed
</p>
</div>
)}
@@ -56,7 +56,7 @@ export default function MilestoneDetailPage() {
<h3>Issues ({issues.length})</h3>
<table>
<thead>
<tr><th>#</th><th></th><th></th><th></th></tr>
<tr><th>#</th><th>Title</th><th>Status</th><th>Priority</th></tr>
</thead>
<tbody>
{issues.map((i) => (
@@ -67,7 +67,7 @@ export default function MilestoneDetailPage() {
<td><span className={`badge priority-${i.priority}`}>{i.priority}</span></td>
</tr>
))}
{issues.length === 0 && <tr><td colSpan={4} className="empty"> issue</td></tr>}
{issues.length === 0 && <tr><td colSpan={4} className="empty">No linked issues</td></tr>}
</tbody>
</table>
</div>

View File

@@ -38,31 +38,31 @@ export default function MilestonesPage() {
return (
<div className="milestones-page">
<div className="page-header">
<h2>🏁 ({milestones.length})</h2>
<h2>🏁 Milestones ({milestones.length})</h2>
<button className="btn-primary" onClick={() => setShowCreate(!showCreate)}>
{showCreate ? '取消' : '+ 新建里程碑'}
{showCreate ? 'Cancel' : '+ NewMilestones'}
</button>
</div>
<div className="filters">
<select value={projectFilter} onChange={(e) => setProjectFilter(e.target.value)}>
<option value=""></option>
<option value="">All projects</option>
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
</div>
{showCreate && (
<form className="inline-form" onSubmit={createMilestone}>
<input required placeholder="里程碑标题" value={form.title}
<input required placeholder="MilestonesTitle" value={form.title}
onChange={(e) => setForm({ ...form, title: e.target.value })} />
<input placeholder="描述(可选)" value={form.description}
<input placeholder="Description (optional)" value={form.description}
onChange={(e) => setForm({ ...form, description: e.target.value })} />
<select value={form.project_id} onChange={(e) => setForm({ ...form, project_id: Number(e.target.value) })}>
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
<input type="date" value={form.due_date}
onChange={(e) => setForm({ ...form, due_date: e.target.value })} />
<button type="submit" className="btn-primary"></button>
<button type="submit" className="btn-primary">Create</button>
</form>
)}
@@ -73,14 +73,14 @@ export default function MilestonesPage() {
<span className={`badge status-${ms.status === 'active' ? 'open' : 'closed'}`}>{ms.status}</span>
<h3>{ms.title}</h3>
</div>
<p className="project-desc">{ms.description || '暂无描述'}</p>
<p className="project-desc">{ms.description || 'No description'}</p>
<div className="project-meta">
{ms.due_date && <span> {dayjs(ms.due_date).format('YYYY-MM-DD')}</span>}
<span> {dayjs(ms.created_at).format('YYYY-MM-DD')}</span>
{ms.due_date && <span>Due {dayjs(ms.due_date).format('YYYY-MM-DD')}</span>}
<span>Created {dayjs(ms.created_at).format('YYYY-MM-DD')}</span>
</div>
</div>
))}
{milestones.length === 0 && <p className="empty"></p>}
{milestones.length === 0 && <p className="empty">No milestones</p>}
</div>
</div>
)

View File

@@ -159,24 +159,24 @@ export default function MonitorPage() {
<div className="stats-grid">
<div className="stat-card total">
<span className="stat-number">{data.issues.total_issues}</span>
<span className="stat-label"> Issues</span>
<span className="stat-label">Total Issues</span>
</div>
<div className="stat-card" style={{ borderLeftColor: 'var(--accent)' }}>
<span className="stat-number">{data.issues.new_issues_24h}</span>
<span className="stat-label">24</span>
<span className="stat-label">New (24h)</span>
</div>
<div className="stat-card" style={{ borderLeftColor: 'var(--success)' }}>
<span className="stat-number">{data.issues.processed_issues_24h}</span>
<span className="stat-label">24</span>
<span className="stat-label">Processed (24h)</span>
</div>
</div>
<div className="section">
<div className="page-header">
<h3>Provider Usage</h3>
<span className="text-dim"> {data.generated_at}</span>
<span className="text-dim">Updated at {data.generated_at}</span>
</div>
{data.providers.length === 0 ? <p className="empty"> provider </p> : (
{data.providers.length === 0 ? <p className="empty">No provider accounts</p> : (
<table>
<thead>
<tr>
@@ -209,8 +209,8 @@ export default function MonitorPage() {
</div>
<div className="section">
<h3></h3>
{data.servers.length === 0 ? <p className="empty"></p> : (
<h3>Server Monitoring</h3>
{data.servers.length === 0 ? <p className="empty">No monitored servers</p> : (
<div className="monitor-grid">
{data.servers.map((s) => (
<div key={s.server_id} className="monitor-card">
@@ -234,11 +234,11 @@ export default function MonitorPage() {
{canAdmin && (
<div className="section">
<h3>Admin </h3>
<h3>Admin</h3>
<div className="monitor-admin">
<div className="monitor-card">
<h4>Provider </h4>
<h4>Provider Accounts</h4>
<div className="inline-form">
<select value={providerForm.provider} onChange={(e) => setProviderForm({ ...providerForm, provider: e.target.value })}>
<option value='openai'>openai</option>
@@ -249,30 +249,30 @@ export default function MonitorPage() {
</select>
<input placeholder='label' value={providerForm.label} onChange={(e) => setProviderForm({ ...providerForm, label: e.target.value })} />
<input placeholder='credential' value={providerForm.credential} onChange={(e) => setProviderForm({ ...providerForm, credential: e.target.value })} />
<button className="btn-primary" onClick={testProvider}></button>
<button className="btn-primary" onClick={addProvider}></button>
<button className="btn-primary" onClick={testProvider}>Test Connection</button>
<button className="btn-primary" onClick={addProvider}>Add Account</button>
</div>
{providerTestMsg && <p className="text-dim">{providerTestMsg}</p>}
<ul>
{providerAccounts.map((p) => (
<li key={p.id}>{p.provider} / {p.label} / {p.credential_masked} <button className="btn-danger" onClick={() => deleteProvider(p.id)}></button></li>
<li key={p.id}>{p.provider} / {p.label} / {p.credential_masked} <button className="btn-danger" onClick={() => deleteProvider(p.id)}>Delete</button></li>
))}
</ul>
</div>
<div className="monitor-card">
<h4></h4>
<h4>Servers</h4>
<div className="inline-form">
<input placeholder='identifier' value={serverForm.identifier} onChange={(e) => setServerForm({ ...serverForm, identifier: e.target.value })} />
<input placeholder='display_name' value={serverForm.display_name} onChange={(e) => setServerForm({ ...serverForm, display_name: e.target.value })} />
<button className="btn-primary" onClick={addServer}></button>
<button className="btn-primary" onClick={addServer}>Add Server</button>
</div>
<ul>
{servers.map((s) => (
<li key={s.server_id}>
{s.display_name} ({s.identifier})
<button className="btn-secondary" onClick={() => createChallenge(s.server_id)} style={{ marginLeft: 8 }}></button>
<button className="btn-danger" onClick={() => deleteServer(s.server_id)} style={{ marginLeft: 8 }}></button>
<button className="btn-secondary" onClick={() => createChallenge(s.server_id)} style={{ marginLeft: 8 }}>Generate Challenge</button>
<button className="btn-danger" onClick={() => deleteServer(s.server_id)} style={{ marginLeft: 8 }}>Delete</button>
</li>
))}
</ul>

View File

@@ -32,16 +32,16 @@ export default function NotificationsPage() {
return (
<div className="notifications-page">
<div className="page-header">
<h2>🔔 {unreadCount > 0 && <span className="badge" style={{ background: 'var(--danger)' }}>{unreadCount}</span>}</h2>
<h2>🔔 Notifications {unreadCount > 0 && <span className="badge" style={{ background: 'var(--danger)' }}>{unreadCount}</span>}</h2>
{unreadCount > 0 && (
<button className="btn-primary" onClick={markAllRead}></button>
<button className="btn-primary" onClick={markAllRead}>Mark all read</button>
)}
</div>
<div className="filters">
<label className="filter-check">
<input type="checkbox" checked={unreadOnly} onChange={(e) => setUnreadOnly(e.target.checked)} />
Show unread only
</label>
</div>
@@ -62,7 +62,7 @@ export default function NotificationsPage() {
</div>
</div>
))}
{notifications.length === 0 && <p className="empty"></p>}
{notifications.length === 0 && <p className="empty">No notifications</p>}
</div>
</div>
)

View File

@@ -31,62 +31,62 @@ export default function ProjectDetailPage() {
setEditing(false)
}
if (!project) return <div className="loading">...</div>
if (!project) return <div className="loading">Loading...</div>
return (
<div className="project-detail">
<button className="btn-back" onClick={() => navigate('/projects')}> </button>
<button className="btn-back" onClick={() => navigate('/projects')}> Back to projects</button>
<div className="issue-header">
{editing ? (
<form className="inline-form" onSubmit={updateProject}>
<input value={editForm.name} onChange={(e) => setEditForm({ ...editForm, name: e.target.value })} required />
<input value={editForm.description} onChange={(e) => setEditForm({ ...editForm, description: e.target.value })} placeholder="描述" />
<button type="submit" className="btn-primary"></button>
<button type="button" className="btn-back" onClick={() => setEditing(false)}></button>
<input value={editForm.description} onChange={(e) => setEditForm({ ...editForm, description: e.target.value })} placeholder="Description" />
<button type="submit" className="btn-primary">Save</button>
<button type="button" className="btn-back" onClick={() => setEditing(false)}>Cancel</button>
</form>
) : (
<>
<h2>📁 {project.name}</h2>
<p style={{ color: 'var(--text-dim)', marginTop: 4 }}>{project.description || '暂无描述'}</p>
<button className="btn-transition" style={{ marginTop: 8 }} onClick={() => setEditing(true)}></button>
<p style={{ color: 'var(--text-dim)', marginTop: 4 }}>{project.description || 'No description'}</p>
<button className="btn-transition" style={{ marginTop: 8 }} onClick={() => setEditing(true)}>Edit</button>
</>
)}
</div>
<div className="section">
<h3> ({members.length})</h3>
<h3>Members ({members.length})</h3>
{members.length > 0 ? (
<div className="member-list">
{members.map((m) => (
<span key={m.id} className="badge">{`用户 #${m.user_id} (${m.role})`}</span>
<span key={m.id} className="badge">{`User #${m.user_id} (${m.role})`}</span>
))}
</div>
) : (
<p className="empty"></p>
<p className="empty">No members</p>
)}
</div>
<div className="section">
<h3> ({milestones.length})</h3>
<h3>Milestones ({milestones.length})</h3>
{milestones.map((ms) => (
<div key={ms.id} className="milestone-item" onClick={() => navigate(`/milestones/${ms.id}`)}>
<span className={`badge status-${ms.status === 'active' ? 'open' : ms.status === 'closed' ? 'closed' : 'in_progress'}`}>{ms.status}</span>
<span className="milestone-title">{ms.title}</span>
{ms.due_date && <span className="text-dim"> · {dayjs(ms.due_date).format('YYYY-MM-DD')}</span>}
{ms.due_date && <span className="text-dim"> · Due {dayjs(ms.due_date).format('YYYY-MM-DD')}</span>}
</div>
))}
{milestones.length === 0 && <p className="empty"></p>}
{milestones.length === 0 && <p className="empty">No milestones</p>}
</div>
<div className="section">
<div className="page-header">
<h3> Issues</h3>
<button className="btn-primary" onClick={() => navigate('/issues/new')}>+ </button>
<h3>Recent Issues</h3>
<button className="btn-primary" onClick={() => navigate('/issues/new')}>+ New</button>
</div>
<table>
<thead>
<tr><th>#</th><th></th><th></th><th></th></tr>
<tr><th>#</th><th>Title</th><th>Status</th><th>Priority</th></tr>
</thead>
<tbody>
{issues.map((i) => (

View File

@@ -27,23 +27,23 @@ export default function ProjectsPage() {
return (
<div className="projects-page">
<div className="page-header">
<h2>📁 ({projects.length})</h2>
<h2>📁 Projects ({projects.length})</h2>
<button className="btn-primary" onClick={() => setShowCreate(!showCreate)}>
{showCreate ? '取消' : '+ 新建项目'}
{showCreate ? 'Cancel' : '+ NewProjects'}
</button>
</div>
{showCreate && (
<form className="inline-form" onSubmit={createProject}>
<input
required placeholder="项目名称" value={form.name}
required placeholder="Project name" value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
/>
<input
placeholder="项目描述(可选)" value={form.description}
placeholder="ProjectsDescription (optional)" value={form.description}
onChange={(e) => setForm({ ...form, description: e.target.value })}
/>
<button type="submit" className="btn-primary"></button>
<button type="submit" className="btn-primary">Create</button>
</form>
)}
@@ -51,13 +51,13 @@ export default function ProjectsPage() {
{projects.map((p) => (
<div key={p.id} className="project-card" onClick={() => navigate(`/projects/${p.id}`)}>
<h3>{p.name}</h3>
<p className="project-desc">{p.description || '暂无描述'}</p>
<p className="project-desc">{p.description || 'No description'}</p>
<div className="project-meta">
<span> {dayjs(p.created_at).format('YYYY-MM-DD')}</span>
<span>Created {dayjs(p.created_at).format('YYYY-MM-DD')}</span>
</div>
</div>
))}
{projects.length === 0 && <p className="empty"></p>}
{projects.length === 0 && <p className="empty">No projects yet. Create one above.</p>}
</div>
</div>
)

View File

@@ -21,7 +21,7 @@ interface SetupForm {
project_description: string
}
const STEPS = ['欢迎', '数据库', '管理员', '项目', '完成']
const STEPS = ['Welcome', 'Database', 'Admin', 'Projects', 'Finish']
export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
const [step, setStep] = useState(0)
@@ -40,7 +40,7 @@ export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
db_database: 'harborforge',
backend_base_url: 'http://127.0.0.1:8000',
project_name: 'Default',
project_description: '默认项目',
project_description: 'Default project',
})
const wizardApi = axios.create({
@@ -59,7 +59,7 @@ export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
setStep(1)
} catch {
setWizardOk(false)
setError(`无法连接 AbstractWizard (${wizardBase})\n请确认已通过 SSH 隧道映射端口:\nssh -L <wizard_port>:127.0.0.1:<wizard_port> user@server`)
setError(`Unable to connect to AbstractWizard (${wizardBase}).\nPlease ensure the SSH tunnel is configured:\nssh -L <wizard_port>:127.0.0.1:<wizard_port> user@server`)
}
}
@@ -98,7 +98,7 @@ export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
setStep(4)
} catch (err: any) {
setError(`保存配置失败: ${err.message}`)
setError(`Failed to save configuration: ${err.message}`)
} finally {
setSaving(false)
}
@@ -108,7 +108,7 @@ export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
<div className="setup-wizard">
<div className="setup-container">
<div className="setup-header">
<h1> HarborForge </h1>
<h1> HarborForge Setup Wizard</h1>
<div className="setup-steps">
{STEPS.map((s, i) => (
<span key={i} className={`setup-step ${i === step ? 'active' : i < step ? 'done' : ''}`}>
@@ -123,17 +123,17 @@ export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
{/* Step 0: Welcome */}
{step === 0 && (
<div className="setup-step-content">
<h2>使 HarborForge</h2>
<p>Agent/</p>
<h2>Welcome to HarborForge</h2>
<p>Agent/Human collaborative task management platform</p>
<div className="setup-info">
<p> SSH AbstractWizard</p>
<p> The setup wizard connects to AbstractWizard via SSH tunnel. Ensure the port is forwarded:</p>
<code>ssh -L &lt;wizard_port&gt;:127.0.0.1:&lt;wizard_port&gt; user@your-server</code>
</div>
<button className="btn-primary" onClick={checkWizard}>
Wizard
Connect to Wizard
</button>
{wizardOk === false && (
<p className="setup-hint"> SSH </p>
<p className="setup-hint">Connection failed. Check the SSH tunnel.</p>
)}
</div>
)}
@@ -141,18 +141,18 @@ export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
{/* Step 1: Database */}
{step === 1 && (
<div className="setup-step-content">
<h2></h2>
<p className="text-dim"> MySQL 使 docker-compose MySQL </p>
<h2>Database configuration</h2>
<p className="text-dim">Configure MySQL connection (docker-compose defaults are fine if using the bundled MySQL).</p>
<div className="setup-form">
<label> <input value={form.db_host} onChange={(e) => set('db_host', e.target.value)} /></label>
<label> <input type="number" value={form.db_port} onChange={(e) => set('db_port', Number(e.target.value))} /></label>
<label> <input value={form.db_user} onChange={(e) => set('db_user', e.target.value)} /></label>
<label> <input type="password" value={form.db_password} onChange={(e) => set('db_password', e.target.value)} /></label>
<label> <input value={form.db_database} onChange={(e) => set('db_database', e.target.value)} /></label>
<label>Host <input value={form.db_host} onChange={(e) => set('db_host', e.target.value)} /></label>
<label>Port <input type="number" value={form.db_port} onChange={(e) => set('db_port', Number(e.target.value))} /></label>
<label>Username <input value={form.db_user} onChange={(e) => set('db_user', e.target.value)} /></label>
<label>Password <input type="password" value={form.db_password} onChange={(e) => set('db_password', e.target.value)} /></label>
<label>Database <input value={form.db_database} onChange={(e) => set('db_database', e.target.value)} /></label>
</div>
<div className="setup-nav">
<button className="btn-back" onClick={() => setStep(0)}></button>
<button className="btn-primary" onClick={() => setStep(2)}></button>
<button className="btn-back" onClick={() => setStep(0)}>Back</button>
<button className="btn-primary" onClick={() => setStep(2)}>Next</button>
</div>
</div>
)}
@@ -160,21 +160,21 @@ export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
{/* Step 2: Admin */}
{step === 2 && (
<div className="setup-step-content">
<h2></h2>
<p className="text-dim"></p>
<h2>Admin account</h2>
<p className="text-dim">Create the first admin user</p>
<div className="setup-form">
<label> <input value={form.admin_username} onChange={(e) => set('admin_username', e.target.value)} required /></label>
<label> <input type="password" value={form.admin_password} onChange={(e) => set('admin_password', e.target.value)} required placeholder="设置管理员密码" /></label>
<label> <input type="email" value={form.admin_email} onChange={(e) => set('admin_email', e.target.value)} placeholder="admin@example.com" /></label>
<label> <input value={form.admin_full_name} onChange={(e) => set('admin_full_name', e.target.value)} /></label>
<label>Username <input value={form.admin_username} onChange={(e) => set('admin_username', e.target.value)} required /></label>
<label>Password <input type="password" value={form.admin_password} onChange={(e) => set('admin_password', e.target.value)} required placeholder="Set admin password" /></label>
<label>Email <input type="email" value={form.admin_email} onChange={(e) => set('admin_email', e.target.value)} placeholder="admin@example.com" /></label>
<label>Full name <input value={form.admin_full_name} onChange={(e) => set('admin_full_name', e.target.value)} /></label>
</div>
<div className="setup-nav">
<button className="btn-back" onClick={() => setStep(1)}></button>
<button className="btn-back" onClick={() => setStep(1)}>Back</button>
<button className="btn-primary" onClick={() => {
if (!form.admin_password) { setError('请设置管理员密码'); return }
if (!form.admin_password) { setError('Please set an admin password'); return }
setError('')
setStep(3)
}}></button>
}}>Next</button>
</div>
</div>
)}
@@ -182,17 +182,17 @@ export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
{/* Step 3: Project */}
{step === 3 && (
<div className="setup-step-content">
<h2></h2>
<p className="text-dim"></p>
<h2>Default project (optional)</h2>
<p className="text-dim">Create an initial project or skip</p>
<div className="setup-form">
<label> Base URL <input value={form.backend_base_url} onChange={(e) => set('backend_base_url', e.target.value)} placeholder="http://127.0.0.1:8000" /></label>
<label> <input value={form.project_name} onChange={(e) => set('project_name', e.target.value)} placeholder="留空则跳过" /></label>
<label> <input value={form.project_description} onChange={(e) => set('project_description', e.target.value)} /></label>
<label>Backend Base URL <input value={form.backend_base_url} onChange={(e) => set('backend_base_url', e.target.value)} placeholder="http://127.0.0.1:8000" /></label>
<label>Project name <input value={form.project_name} onChange={(e) => set('project_name', e.target.value)} placeholder="Leave blank to skip" /></label>
<label>ProjectsDescription <input value={form.project_description} onChange={(e) => set('project_description', e.target.value)} /></label>
</div>
<div className="setup-nav">
<button className="btn-back" onClick={() => setStep(2)}></button>
<button className="btn-back" onClick={() => setStep(2)}>Back</button>
<button className="btn-primary" onClick={saveConfig} disabled={saving}>
{saving ? '保存中...' : '完成配置'}
{saving ? 'Saving...' : 'Finish setup'}
</button>
</div>
</div>
@@ -202,16 +202,16 @@ export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
{step === 4 && (
<div className="setup-step-content">
<div className="setup-done">
<h2> </h2>
<p> AbstractWizard</p>
<h2> Setup complete!</h2>
<p>Configuration saved to AbstractWizard.</p>
<div className="setup-info">
<p></p>
<p>Restart services on the server:</p>
<code>docker compose restart</code>
<p style={{ marginTop: '1rem' }}></p>
<p>: <strong>{form.admin_username}</strong></p>
<p style={{ marginTop: '1rem' }}>After the backend starts, refresh this page to go to login.</p>
<p>Admin account: <strong>{form.admin_username}</strong></p>
</div>
<button className="btn-primary" onClick={onComplete}>
Refresh to check
</button>
</div>
</div>