-
Notifications
You must be signed in to change notification settings - Fork 27
Expand file tree
/
Copy pathPMIntakeLeftSidebar.tsx
More file actions
219 lines (210 loc) · 9.32 KB
/
PMIntakeLeftSidebar.tsx
File metadata and controls
219 lines (210 loc) · 9.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
import type { MouseEvent } from "react";
import PmStageRail from "../../../components/pm/PmStageRail";
import { useDashboardLocale } from "../../../components/DashboardLocaleContext";
import { Button } from "../../../components/ui/button";
import { Input } from "../../../components/ui/input";
import type { PmSessionSummary } from "../../../lib/types";
import { buildSessionDisplayLabel, buildSessionMiniChain, compactSessionId, summarizeSession } from "./PMIntakeFeature.shared";
type Props = {
intakeId: string;
chatFlowBusy: boolean;
newConversationBusy: boolean;
onStartNewConversation: () => void;
objective: string;
workspacePath: string;
repoName: string;
onWorkspacePathChange: (value: string) => void;
onRepoNameChange: (value: string) => void;
stage: "discover" | "clarify" | "execute" | "verify";
sessionHistoryError: string;
newConversationError: string;
newConversationNotice: string;
historyBusy: boolean;
sessionHistory: PmSessionSummary[];
onSessionSelect: (sessionId: string) => void;
onFocusInput: () => void;
};
export default function PMIntakeLeftSidebar(props: Props) {
const { locale } = useDashboardLocale();
const {
intakeId,
chatFlowBusy,
newConversationBusy,
onStartNewConversation,
objective,
workspacePath,
repoName,
onWorkspacePathChange,
onRepoNameChange,
stage,
sessionHistoryError,
newConversationError,
newConversationNotice,
historyBusy,
sessionHistory,
onSessionSelect,
onFocusInput,
} = props;
const activeSession = sessionHistory.find((session) => session.pm_session_id === intakeId);
const showBlockingHistoryError = Boolean(sessionHistoryError) && (Boolean(intakeId) || sessionHistory.length > 0);
const showFirstRunHistoryNotice = Boolean(sessionHistoryError) && !showBlockingHistoryError;
const activeSessionLabel = intakeId
? `${locale === "zh-CN" ? "当前会话" : "Current session"}: ${
activeSession
? buildSessionDisplayLabel(activeSession)
: buildSessionDisplayLabel({ objective, project_key: repoName, pm_session_id: intakeId })
}`
: locale === "zh-CN" ? "当前会话:草稿(未发送)" : "Current session: Draft (unsent)";
const handleDraftSessionClick = (event: MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
onFocusInput();
};
const handleSessionItemClick =
(sessionId: string) =>
(event: MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
onSessionSelect(sessionId);
};
return (
<aside className="pm-claude-left" aria-label="Session history sidebar">
<header className="pm-sidebar-header">
<h2 className="pm-sidebar-title">PM</h2>
<Button
variant="default"
className="pm-new-chat-btn"
disabled={chatFlowBusy || newConversationBusy}
onClick={() => onStartNewConversation()}
data-testid="pm-new-conversation"
>
{newConversationBusy ? (locale === "zh-CN" ? "正在创建..." : "Creating...") : locale === "zh-CN" ? "+ 新会话" : "+ New chat"}
</Button>
</header>
<div className="pm-workspace-bind">
<div className="pm-workspace-row">
<label className="sr-only" htmlFor="pm-workspace-path-input">
{locale === "zh-CN" ? "仓库路径" : "Workspace path"}
</label>
<Input
id="pm-workspace-path-input"
name="workspace_path"
className="pm-input pm-input-compact"
value={workspacePath}
onChange={(event) => onWorkspacePathChange(event.target.value)}
placeholder={locale === "zh-CN" ? "仓库路径" : "Workspace path"}
aria-label={locale === "zh-CN" ? "仓库路径" : "Workspace path"}
/>
<label className="sr-only" htmlFor="pm-repo-input">
{locale === "zh-CN" ? "仓库标识" : "Repo"}
</label>
<Input
id="pm-repo-input"
name="repo_name"
className="pm-input pm-input-compact pm-repo-input"
value={repoName}
onChange={(event) => onRepoNameChange(event.target.value)}
placeholder={locale === "zh-CN" ? "仓库标识" : "Repo"}
aria-label={locale === "zh-CN" ? "仓库标识" : "Repository slug"}
/>
</div>
</div>
<PmStageRail stage={stage} />
<p className="mono muted" role="status" aria-live="polite" data-testid="pm-sidebar-active-session-indicator">
{activeSessionLabel}
</p>
{showBlockingHistoryError ? <p className="alert alert-danger" role="alert">{sessionHistoryError}</p> : null}
{showFirstRunHistoryNotice ? (
<p className="alert alert-warning" role="status">
{locale === "zh-CN"
? "会话历史暂时不可用。你仍然可以直接发出第一条请求,系统会创建正式会话。"
: "Session history is temporarily unavailable. You can still start the first request and create the formal session."}
</p>
) : null}
{newConversationError && (
<p className="alert alert-danger" role="alert" data-testid="pm-new-conversation-error">
{newConversationError}
</p>
)}
{newConversationNotice && !newConversationError && (
<p className="alert alert-success" role="status" aria-live="polite" data-testid="pm-new-conversation-notice">
{newConversationNotice}
</p>
)}
<nav aria-label={locale === "zh-CN" ? "会话历史列表" : "Session history list"}>
<ul className="pm-session-list" aria-label={locale === "zh-CN" ? "会话选择器" : "Session picker"}>
<li>
<Button
variant="unstyled"
className={`pm-session-item${!intakeId ? " is-active" : ""}`}
data-testid="pm-session-item-draft"
disabled={chatFlowBusy}
onClick={handleDraftSessionClick}
aria-current={!intakeId ? "page" : undefined}
data-draft-focus-only="true"
aria-label={locale === "zh-CN" ? "草稿会话,聚焦输入框" : "Draft session, focus the composer"}
>
<div className="pm-session-item-row">
<strong className="pm-session-id">{locale === "zh-CN" ? "草稿会话(开始输入)" : "Draft session (start typing)"}</strong>
</div>
<span className="pm-session-meta">
{locale === "zh-CN"
? "这里只会聚焦输入框。发送第一条请求后,系统才会创建正式会话。"
: "Focuses the composer only. Sending the first request creates the formal session."}
</span>
</Button>
</li>
{historyBusy && sessionHistory.length === 0 ? (
<li className="pm-session-loading">
<div role="status" aria-live="polite">
<p>{locale === "zh-CN" ? "正在加载会话历史" : "Loading session history"}</p>
<div className="skeleton skeleton-row" />
<div className="skeleton skeleton-row" />
</div>
</li>
) : sessionHistory.length === 0 ? (
<li className="pm-session-empty">
{locale === "zh-CN" ? "当前还没有历史会话。先发送第一条请求开始。" : "No previous sessions yet. Send the first request to start."}
</li>
) : (
sessionHistory.map((session) => {
const isActive = session.pm_session_id === intakeId;
const miniChain = buildSessionMiniChain(session);
const sessionDisplayLabel = buildSessionDisplayLabel(session);
const sessionCompactId = compactSessionId(session.pm_session_id);
return (
<li key={session.pm_session_id}>
<Button
variant="unstyled"
className={`pm-session-item${isActive ? " is-active" : ""}`}
data-testid={`pm-session-item-${session.pm_session_id}`}
disabled={chatFlowBusy}
onClick={handleSessionItemClick(session.pm_session_id)}
aria-current={isActive ? "page" : undefined}
data-state={isActive ? "active" : "inactive"}
data-pm-session-id={session.pm_session_id}
aria-label={locale === "zh-CN" ? `历史会话 ${session.pm_session_id}` : `Historical session ${session.pm_session_id}`}
>
<div className="pm-session-item-row">
<strong className="pm-session-id" title={session.pm_session_id}>
{sessionDisplayLabel}
</strong>
<span className="pm-mini-chain" aria-hidden="true">
{miniChain.map((state, index) => (
<span key={index} className={`pm-mini-node is-${state}`} />
))}
</span>
</div>
<span className="pm-session-meta">
{`ID ${sessionCompactId} · ${summarizeSession(session)}`}
</span>
</Button>
</li>
);
})
)}
</ul>
</nav>
</aside>
);
}