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') { if (appState === 'checking') {
return <div className="loading">...</div> return <div className="loading">Checking configuration status...</div>
} }
if (appState === 'setup') { if (appState === 'setup') {
return <SetupWizardPage wizardBase={WIZARD_BASE} onComplete={checkInitialized} /> return <SetupWizardPage wizardBase={WIZARD_BASE} onComplete={checkInitialized} />
} }
if (loading) return <div className="loading">...</div> if (loading) return <div className="loading">Loading...</div>
if (!user) { if (!user) {
return ( return (

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,12 +33,12 @@ export default function IssuesPage() {
<div className="issues-page"> <div className="issues-page">
<div className="page-header"> <div className="page-header">
<h2>📋 Issues ({total})</h2> <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>
<div className="filters"> <div className="filters">
<select value={statusFilter} onChange={(e) => { setStatusFilter(e.target.value); setPage(1) }}> <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="open">Open</option>
<option value="in_progress">In Progress</option> <option value="in_progress">In Progress</option>
<option value="resolved">Resolved</option> <option value="resolved">Resolved</option>
@@ -49,7 +49,7 @@ export default function IssuesPage() {
<table className="issues-table"> <table className="issues-table">
<thead> <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> </thead>
<tbody> <tbody>
{issues.map((i) => ( {issues.map((i) => (
@@ -68,9 +68,9 @@ export default function IssuesPage() {
{totalPages > 1 && ( {totalPages > 1 && (
<div className="pagination"> <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> <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>
)} )}
</div> </div>

View File

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

View File

@@ -17,37 +17,37 @@ export default function MilestoneDetailPage() {
api.get<Issue[]>(`/milestones/${id}/issues`).then(({ data }) => setIssues(data)).catch(() => {}) api.get<Issue[]>(`/milestones/${id}/issues`).then(({ data }) => setIssues(data)).catch(() => {})
}, [id]) }, [id])
if (!milestone) return <div className="loading">...</div> if (!milestone) return <div className="loading">Loading...</div>
return ( return (
<div className="milestone-detail"> <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"> <div className="issue-header">
<h2>🏁 {milestone.title}</h2> <h2>🏁 {milestone.title}</h2>
<div className="issue-meta"> <div className="issue-meta">
<span className={`badge status-${milestone.status === 'active' ? 'open' : 'closed'}`}>{milestone.status}</span> <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>
</div> </div>
{milestone.description && ( {milestone.description && (
<div className="section"> <div className="section">
<h3></h3> <h3>Description</h3>
<p>{milestone.description}</p> <p>{milestone.description}</p>
</div> </div>
)} )}
{progress && ( {progress && (
<div className="section"> <div className="section">
<h3></h3> <h3>Progress</h3>
<div className="progress-bar-container"> <div className="progress-bar-container">
<div className="progress-bar" style={{ width: `${progress.progress_percent}%` }}> <div className="progress-bar" style={{ width: `${progress.progress_percent}%` }}>
{progress.progress_percent.toFixed(0)}% {progress.progress_percent.toFixed(0)}%
</div> </div>
</div> </div>
<p className="text-dim" style={{ marginTop: 8 }}> <p className="text-dim" style={{ marginTop: 8 }}>
{progress.completed_issues} / {progress.total_issues} issues {progress.completed_issues} / {progress.total_issues} issues completed
</p> </p>
</div> </div>
)} )}
@@ -56,7 +56,7 @@ export default function MilestoneDetailPage() {
<h3>Issues ({issues.length})</h3> <h3>Issues ({issues.length})</h3>
<table> <table>
<thead> <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> </thead>
<tbody> <tbody>
{issues.map((i) => ( {issues.map((i) => (
@@ -67,7 +67,7 @@ export default function MilestoneDetailPage() {
<td><span className={`badge priority-${i.priority}`}>{i.priority}</span></td> <td><span className={`badge priority-${i.priority}`}>{i.priority}</span></td>
</tr> </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> </tbody>
</table> </table>
</div> </div>

View File

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

View File

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

View File

@@ -32,16 +32,16 @@ export default function NotificationsPage() {
return ( return (
<div className="notifications-page"> <div className="notifications-page">
<div className="page-header"> <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 && ( {unreadCount > 0 && (
<button className="btn-primary" onClick={markAllRead}></button> <button className="btn-primary" onClick={markAllRead}>Mark all read</button>
)} )}
</div> </div>
<div className="filters"> <div className="filters">
<label className="filter-check"> <label className="filter-check">
<input type="checkbox" checked={unreadOnly} onChange={(e) => setUnreadOnly(e.target.checked)} /> <input type="checkbox" checked={unreadOnly} onChange={(e) => setUnreadOnly(e.target.checked)} />
Show unread only
</label> </label>
</div> </div>
@@ -62,7 +62,7 @@ export default function NotificationsPage() {
</div> </div>
</div> </div>
))} ))}
{notifications.length === 0 && <p className="empty"></p>} {notifications.length === 0 && <p className="empty">No notifications</p>}
</div> </div>
</div> </div>
) )

View File

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

View File

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

View File

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