codex-weixin 0.2.6 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.en.md +4 -1
- package/README.md +6 -1
- package/dist/bridge/service.d.ts +1 -0
- package/dist/bridge/service.js +44 -4
- package/dist/bridge/service.js.map +1 -1
- package/dist/codex/app-server-runner.d.ts +8 -0
- package/dist/codex/app-server-runner.js +91 -5
- package/dist/codex/app-server-runner.js.map +1 -1
- package/dist/codex/exec-runner.d.ts +2 -0
- package/dist/codex/exec-runner.js.map +1 -1
- package/dist/codex/runner.js +7 -2
- package/dist/codex/runner.js.map +1 -1
- package/dist/server/account-manager.d.ts +2 -1
- package/dist/server/account-manager.js +9 -2
- package/dist/server/account-manager.js.map +1 -1
- package/dist/server/http-server.js +48 -4
- package/dist/server/http-server.js.map +1 -1
- package/dist/state/config.d.ts +1 -0
- package/dist/state/config.js +2 -0
- package/dist/state/config.js.map +1 -1
- package/dist/state/runtime-state.d.ts +3 -0
- package/dist/state/runtime-state.js +20 -0
- package/dist/state/runtime-state.js.map +1 -1
- package/dist/web/app.js +155 -10
- package/dist/web/index.html +15 -0
- package/dist/web/styles.css +19 -1
- package/package.json +1 -1
package/dist/web/app.js
CHANGED
|
@@ -25,6 +25,7 @@ const MAX_CHAT_FILE_BYTES = 50 * 1024 * 1024;
|
|
|
25
25
|
const DISMISSED_UPDATE_KEY = "codex-weixin.dismissed-update";
|
|
26
26
|
const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
27
27
|
const UPDATE_RECONNECT_TIMEOUT_MS = 90 * 1000;
|
|
28
|
+
let streamingRenderFrame = 0;
|
|
28
29
|
|
|
29
30
|
const els = {};
|
|
30
31
|
|
|
@@ -48,6 +49,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
48
49
|
sessionRuntimeToolbar: document.querySelector("#sessionRuntimeToolbar"),
|
|
49
50
|
sessionModelInput: document.querySelector("#sessionModelInput"),
|
|
50
51
|
sessionEffortInput: document.querySelector("#sessionEffortInput"),
|
|
52
|
+
sessionStreamInput: document.querySelector("#sessionStreamInput"),
|
|
51
53
|
runningAccountMetric: document.querySelector("#runningAccountMetric"),
|
|
52
54
|
sessionMetric: document.querySelector("#sessionMetric"),
|
|
53
55
|
workspaceMetric: document.querySelector("#workspaceMetric"),
|
|
@@ -80,6 +82,7 @@ function bindEvents() {
|
|
|
80
82
|
document.querySelector("#modelInput").addEventListener("change", () => renderEffortOptions(""));
|
|
81
83
|
els.sessionModelInput.addEventListener("change", () => void handleSessionModelChange());
|
|
82
84
|
els.sessionEffortInput.addEventListener("change", () => void saveSessionRuntimeSettings());
|
|
85
|
+
els.sessionStreamInput.addEventListener("change", () => void saveSessionRuntimeSettings());
|
|
83
86
|
document.querySelector("#accountForm").addEventListener("submit", (event) => void saveAccountRemark(event));
|
|
84
87
|
document.querySelector("#sessionForm").addEventListener("submit", (event) => void saveSession(event));
|
|
85
88
|
document.querySelector("#sessionSenderInput").addEventListener("change", updateNewSessionDefaultTitle);
|
|
@@ -441,6 +444,7 @@ function renderSettings() {
|
|
|
441
444
|
document.querySelector("#allowedWorkspacesInput").value = (state.config.allowedWorkspaces || []).join("\n");
|
|
442
445
|
document.querySelector("#backendInput").value = state.config.codexBackend || "auto";
|
|
443
446
|
document.querySelector("#sandboxInput").value = state.config.codexExecSandbox || "";
|
|
447
|
+
document.querySelector("#streamRepliesInput").checked = Boolean(state.config.streamReplies);
|
|
444
448
|
renderModelOptions();
|
|
445
449
|
document.querySelector("#effectiveModelValue").textContent = state.codexRuntime?.model || state.config.model || "Codex 默认";
|
|
446
450
|
document.querySelector("#effectiveEffortValue").textContent = state.codexRuntime?.effort || state.config.effort || "Codex 默认";
|
|
@@ -691,23 +695,79 @@ function renderChatPanel() {
|
|
|
691
695
|
return;
|
|
692
696
|
}
|
|
693
697
|
const renderKey = `messages:${sessionKey(session)}:${responding}:${JSON.stringify(state.sessionMessages)}`;
|
|
694
|
-
const html = state.sessionMessages
|
|
698
|
+
const html = renderConversationMessages(state.sessionMessages, responding) + (responding ? renderTypingIndicator() : "");
|
|
699
|
+
setChatMessagesHtml(html, renderKey);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function renderConversationMessages(messages, responding) {
|
|
703
|
+
const html = [];
|
|
704
|
+
let lastUserCreatedAt;
|
|
705
|
+
let index = 0;
|
|
706
|
+
while (index < messages.length) {
|
|
707
|
+
const message = messages[index];
|
|
708
|
+
if (message.kind === "progress") {
|
|
709
|
+
const progress = [];
|
|
710
|
+
while (index < messages.length && messages[index].kind === "progress") {
|
|
711
|
+
progress.push(messages[index]);
|
|
712
|
+
index += 1;
|
|
713
|
+
}
|
|
714
|
+
const nextMessage = messages[index];
|
|
715
|
+
const active = responding && !nextMessage;
|
|
716
|
+
const completedAt = nextMessage?.role === "assistant"
|
|
717
|
+
? nextMessage.createdAt
|
|
718
|
+
: progress.at(-1)?.createdAt;
|
|
719
|
+
html.push(renderProgressGroup(progress, lastUserCreatedAt, completedAt, active));
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
if (message.role === "user") lastUserCreatedAt = message.createdAt;
|
|
723
|
+
html.push(renderChatMessage(message));
|
|
724
|
+
index += 1;
|
|
725
|
+
}
|
|
726
|
+
return html.join("");
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function renderChatMessage(message) {
|
|
730
|
+
return `<article class="chat-message is-${escapeAttr(message.role)}${message.attachments?.length ? " has-attachments" : ""}">
|
|
695
731
|
<div class="message-meta"><span>${message.role === "user" ? "你" : "Codex"}</span>${message.createdAt ? `<time datetime="${escapeAttr(message.createdAt)}">${escapeHtml(messageTime(message.createdAt))}</time>` : ""}</div>
|
|
696
732
|
<div class="message-bubble">${message.text ? renderMarkdown(message.text) : ""}${renderMessageAttachments(message.attachments)}</div>
|
|
697
|
-
</article
|
|
698
|
-
|
|
733
|
+
</article>`;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function renderProgressGroup(messages, startedAt, completedAt, active) {
|
|
737
|
+
const duration = formatProcessingDuration(startedAt, active ? new Date().toISOString() : completedAt);
|
|
738
|
+
return `<details class="chat-progress-group"${active ? " open" : ""}>
|
|
739
|
+
<summary>
|
|
740
|
+
<span class="progress-summary-title"><i data-lucide="chevron-right"></i>处理过程</span>
|
|
741
|
+
<span>${active ? "已处理" : "处理用时"} ${escapeHtml(duration)}</span>
|
|
742
|
+
</summary>
|
|
743
|
+
<ol class="progress-list">${messages.map((message) => `<li>${renderMarkdown(message.text)}</li>`).join("")}</ol>
|
|
744
|
+
</details>`;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function formatProcessingDuration(startValue, endValue) {
|
|
748
|
+
const start = new Date(startValue || "").getTime();
|
|
749
|
+
const end = new Date(endValue || "").getTime();
|
|
750
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return "--";
|
|
751
|
+
const seconds = Math.max(0, Math.round((end - start) / 1000));
|
|
752
|
+
if (seconds < 60) return `${seconds} 秒`;
|
|
753
|
+
const minutes = Math.floor(seconds / 60);
|
|
754
|
+
const remaining = seconds % 60;
|
|
755
|
+
return remaining ? `${minutes} 分 ${remaining} 秒` : `${minutes} 分钟`;
|
|
699
756
|
}
|
|
700
757
|
|
|
701
758
|
function renderSessionRuntimeControls(session) {
|
|
702
759
|
const disabled = !session || state.sendingMessage || state.savingSessionRuntime;
|
|
703
760
|
els.sessionModelInput.disabled = disabled;
|
|
704
761
|
els.sessionEffortInput.disabled = disabled;
|
|
762
|
+
els.sessionStreamInput.disabled = disabled;
|
|
705
763
|
const renderKey = session ? [
|
|
706
764
|
sessionKey(session),
|
|
707
765
|
session.model || "",
|
|
708
766
|
session.effort || "",
|
|
767
|
+
typeof session.streamReplies === "boolean" ? String(session.streamReplies) : "inherit",
|
|
709
768
|
state.config?.model || "",
|
|
710
769
|
state.config?.effort || "",
|
|
770
|
+
String(Boolean(state.config?.streamReplies)),
|
|
711
771
|
state.codexRuntime?.model || "",
|
|
712
772
|
state.codexRuntime?.effort || "",
|
|
713
773
|
state.codexModels.length
|
|
@@ -718,6 +778,7 @@ function renderSessionRuntimeControls(session) {
|
|
|
718
778
|
if (!session) {
|
|
719
779
|
els.sessionModelInput.innerHTML = '<option value="">选择会话后设置</option>';
|
|
720
780
|
els.sessionEffortInput.innerHTML = '<option value="">选择会话后设置</option>';
|
|
781
|
+
els.sessionStreamInput.innerHTML = '<option value="">选择会话后设置</option>';
|
|
721
782
|
return;
|
|
722
783
|
}
|
|
723
784
|
|
|
@@ -739,6 +800,14 @@ function renderSessionRuntimeControls(session) {
|
|
|
739
800
|
els.sessionModelInput.innerHTML = options.map((option) => `<option value="${escapeAttr(option.value)}"${option.title ? ` title="${escapeAttr(option.title)}"` : ""}>${escapeHtml(option.label)}</option>`).join("");
|
|
740
801
|
els.sessionModelInput.value = session.model || "";
|
|
741
802
|
setSessionEffortOptions(session.model || "", session.effort || "");
|
|
803
|
+
els.sessionStreamInput.innerHTML = [
|
|
804
|
+
`<option value="">继承全局(${state.config?.streamReplies ? "开启" : "关闭"})</option>`,
|
|
805
|
+
'<option value="on">开启</option>',
|
|
806
|
+
'<option value="off">关闭</option>'
|
|
807
|
+
].join("");
|
|
808
|
+
els.sessionStreamInput.value = typeof session.streamReplies === "boolean"
|
|
809
|
+
? session.streamReplies ? "on" : "off"
|
|
810
|
+
: "";
|
|
742
811
|
}
|
|
743
812
|
|
|
744
813
|
function setSessionEffortOptions(modelOverride, preferredEffort) {
|
|
@@ -783,18 +852,23 @@ async function saveSessionRuntimeSettings() {
|
|
|
783
852
|
const key = sessionKey(session);
|
|
784
853
|
const model = els.sessionModelInput.value;
|
|
785
854
|
const effort = els.sessionEffortInput.value;
|
|
855
|
+
const stream = els.sessionStreamInput.value;
|
|
786
856
|
state.savingSessionRuntime = true;
|
|
787
857
|
renderChatPanel();
|
|
788
858
|
try {
|
|
789
859
|
const result = await api(`/api/sessions/${encodeURIComponent(session.accountId)}/${encodeURIComponent(session.id)}`, {
|
|
790
860
|
method: "PATCH",
|
|
791
|
-
body: {
|
|
861
|
+
body: {
|
|
862
|
+
model: model || null,
|
|
863
|
+
effort: effort || null,
|
|
864
|
+
streamReplies: stream ? stream === "on" : null
|
|
865
|
+
}
|
|
792
866
|
});
|
|
793
867
|
const index = state.sessions.findIndex((candidate) => sessionKey(candidate) === key);
|
|
794
868
|
if (index >= 0) state.sessions[index] = result.session;
|
|
795
869
|
els.sessionRuntimeToolbar.dataset.renderKey = "";
|
|
796
870
|
renderSessions();
|
|
797
|
-
toast("
|
|
871
|
+
toast("会话设置已更新");
|
|
798
872
|
} catch (error) {
|
|
799
873
|
toast(error.message, true);
|
|
800
874
|
await refreshData(false);
|
|
@@ -859,6 +933,8 @@ async function sendSessionMessage(event) {
|
|
|
859
933
|
const files = [...state.chatFiles];
|
|
860
934
|
if (!session || (!text && !files.length) || state.sendingMessage) return;
|
|
861
935
|
const key = sessionKey(session);
|
|
936
|
+
const streaming = session.streamReplies ?? Boolean(state.config?.streamReplies);
|
|
937
|
+
let progressSequence = 0;
|
|
862
938
|
state.sendingMessage = true;
|
|
863
939
|
state.sessionMessages.push({
|
|
864
940
|
id: `pending-${Date.now()}`,
|
|
@@ -881,10 +957,29 @@ async function sendSessionMessage(event) {
|
|
|
881
957
|
const body = new FormData();
|
|
882
958
|
body.append("text", text);
|
|
883
959
|
files.forEach((file) => body.append("files", file, file.name));
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
body
|
|
887
|
-
|
|
960
|
+
const url = `/api/sessions/${encodeURIComponent(session.accountId)}/${encodeURIComponent(session.id)}/messages`;
|
|
961
|
+
if (streaming) {
|
|
962
|
+
await streamApi(`${url}?stream=1`, { method: "POST", body }, (streamEvent) => {
|
|
963
|
+
if (state.selectedSessionKey !== key) return;
|
|
964
|
+
if (streamEvent.type === "progress" && streamEvent.message?.trim()) {
|
|
965
|
+
state.sessionMessages.push({
|
|
966
|
+
id: `progress-${Date.now()}-${progressSequence++}`,
|
|
967
|
+
role: "assistant",
|
|
968
|
+
text: streamEvent.message.trim(),
|
|
969
|
+
kind: "progress",
|
|
970
|
+
createdAt: new Date().toISOString(),
|
|
971
|
+
attachments: []
|
|
972
|
+
});
|
|
973
|
+
scheduleStreamingRender();
|
|
974
|
+
}
|
|
975
|
+
if (streamEvent.type === "done" && streamEvent.result?.message) {
|
|
976
|
+
state.sessionMessages.push(streamEvent.result.message);
|
|
977
|
+
scheduleStreamingRender();
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
} else {
|
|
981
|
+
await api(url, { method: "POST", body });
|
|
982
|
+
}
|
|
888
983
|
await refreshData(false);
|
|
889
984
|
if (state.selectedSessionKey === key) {
|
|
890
985
|
await loadSelectedSessionMessages();
|
|
@@ -903,6 +998,15 @@ async function sendSessionMessage(event) {
|
|
|
903
998
|
}
|
|
904
999
|
}
|
|
905
1000
|
|
|
1001
|
+
function scheduleStreamingRender() {
|
|
1002
|
+
if (streamingRenderFrame) return;
|
|
1003
|
+
streamingRenderFrame = requestAnimationFrame(() => {
|
|
1004
|
+
streamingRenderFrame = 0;
|
|
1005
|
+
renderChatPanel();
|
|
1006
|
+
scrollChatToEnd();
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
|
|
906
1010
|
function handleChatFileSelection(event) {
|
|
907
1011
|
const nextFiles = [...event.target.files];
|
|
908
1012
|
event.target.value = "";
|
|
@@ -1115,7 +1219,8 @@ async function saveSettings(event) {
|
|
|
1115
1219
|
codexBackend: document.querySelector("#backendInput").value,
|
|
1116
1220
|
codexExecSandbox: document.querySelector("#sandboxInput").value || null,
|
|
1117
1221
|
model: document.querySelector("#modelInput").value.trim(),
|
|
1118
|
-
effort: document.querySelector("#effortInput").value.trim()
|
|
1222
|
+
effort: document.querySelector("#effortInput").value.trim(),
|
|
1223
|
+
streamReplies: document.querySelector("#streamRepliesInput").checked
|
|
1119
1224
|
}
|
|
1120
1225
|
});
|
|
1121
1226
|
state.config = result.config;
|
|
@@ -1159,6 +1264,46 @@ async function api(url, options = {}) {
|
|
|
1159
1264
|
return data;
|
|
1160
1265
|
}
|
|
1161
1266
|
|
|
1267
|
+
async function streamApi(url, options, onEvent) {
|
|
1268
|
+
const headers = {};
|
|
1269
|
+
if (state.requestToken) headers["X-Codex-Weixin-Token"] = state.requestToken;
|
|
1270
|
+
const response = await fetch(url, {
|
|
1271
|
+
method: options.method || "POST",
|
|
1272
|
+
headers,
|
|
1273
|
+
body: options.body
|
|
1274
|
+
});
|
|
1275
|
+
const contentType = response.headers.get("content-type") || "";
|
|
1276
|
+
if (!contentType.startsWith("application/x-ndjson")) {
|
|
1277
|
+
const data = await response.json().catch(() => ({}));
|
|
1278
|
+
if (!response.ok) throw new Error(data.error || `请求失败 (${response.status})`);
|
|
1279
|
+
return data.result;
|
|
1280
|
+
}
|
|
1281
|
+
if (!response.ok || !response.body) {
|
|
1282
|
+
throw new Error(`请求失败 (${response.status})`);
|
|
1283
|
+
}
|
|
1284
|
+
const reader = response.body.getReader();
|
|
1285
|
+
const decoder = new TextDecoder();
|
|
1286
|
+
let buffer = "";
|
|
1287
|
+
let result;
|
|
1288
|
+
const consumeLine = (line) => {
|
|
1289
|
+
if (!line.trim()) return;
|
|
1290
|
+
const streamEvent = JSON.parse(line);
|
|
1291
|
+
if (streamEvent.type === "error") throw new Error(streamEvent.error || "过程进度失败");
|
|
1292
|
+
onEvent(streamEvent);
|
|
1293
|
+
if (streamEvent.type === "done") result = streamEvent.result;
|
|
1294
|
+
};
|
|
1295
|
+
while (true) {
|
|
1296
|
+
const { value, done } = await reader.read();
|
|
1297
|
+
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
|
|
1298
|
+
const lines = buffer.split(/\r?\n/);
|
|
1299
|
+
buffer = lines.pop() || "";
|
|
1300
|
+
for (const line of lines) consumeLine(line);
|
|
1301
|
+
if (done) break;
|
|
1302
|
+
}
|
|
1303
|
+
if (buffer) consumeLine(buffer);
|
|
1304
|
+
return result;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1162
1307
|
function emptyState(icon, title, description = "", action = "") {
|
|
1163
1308
|
return `<div class="empty-state"><div class="empty-state-inner"><span class="empty-icon"><i data-lucide="${escapeAttr(icon)}"></i></span><h2>${escapeHtml(title)}</h2>${description ? `<p>${escapeHtml(description)}</p>` : ""}${action}</div></div>`;
|
|
1164
1309
|
}
|
package/dist/web/index.html
CHANGED
|
@@ -109,6 +109,14 @@
|
|
|
109
109
|
<span>推理强度</span>
|
|
110
110
|
<select id="sessionEffortInput" disabled><option value="">继承全局设置</option></select>
|
|
111
111
|
</label>
|
|
112
|
+
<label>
|
|
113
|
+
<span>过程进度</span>
|
|
114
|
+
<select id="sessionStreamInput" disabled>
|
|
115
|
+
<option value="">继承全局设置</option>
|
|
116
|
+
<option value="on">开启</option>
|
|
117
|
+
<option value="off">关闭</option>
|
|
118
|
+
</select>
|
|
119
|
+
</label>
|
|
112
120
|
</div>
|
|
113
121
|
<div class="chat-messages" id="chatMessages" aria-live="polite"></div>
|
|
114
122
|
<form class="chat-composer" id="chatComposer">
|
|
@@ -184,6 +192,13 @@
|
|
|
184
192
|
</select>
|
|
185
193
|
<small class="field-hint effective-setting"><span>当前生效</span><strong id="effectiveEffortValue">正在读取</strong></small>
|
|
186
194
|
</label>
|
|
195
|
+
<label class="toggle-field" for="streamRepliesInput">
|
|
196
|
+
<span>过程进度</span>
|
|
197
|
+
<span class="toggle-control">
|
|
198
|
+
<input id="streamRepliesInput" name="streamReplies" type="checkbox">
|
|
199
|
+
<span aria-hidden="true"></span>
|
|
200
|
+
</span>
|
|
201
|
+
</label>
|
|
187
202
|
</div>
|
|
188
203
|
</div>
|
|
189
204
|
<div class="form-actions">
|
package/dist/web/styles.css
CHANGED
|
@@ -165,7 +165,7 @@ h2 { margin: 0; font-size: 19px; line-height: 1.3; }
|
|
|
165
165
|
.chat-header { min-height: 82px; padding: 17px 20px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; gap: 18px; }
|
|
166
166
|
.chat-header h2 { max-width: 620px; overflow: hidden; font-size: 17px; text-overflow: ellipsis; white-space: nowrap; }
|
|
167
167
|
.chat-context { max-width: 720px; margin: 5px 0 0; overflow: hidden; color: var(--ink-soft); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
|
|
168
|
-
.chat-runtime-toolbar { min-height: 54px; padding: 9px 20px; border-bottom: 1px solid var(--line); background: #f7f9f7; display: grid; grid-template-columns: auto minmax(
|
|
168
|
+
.chat-runtime-toolbar { min-height: 54px; padding: 9px 20px; border-bottom: 1px solid var(--line); background: #f7f9f7; display: grid; grid-template-columns: auto minmax(170px, 1fr) minmax(150px, .72fr) minmax(130px, .55fr); align-items: center; gap: 12px; }
|
|
169
169
|
.chat-runtime-title { color: var(--ink-soft); display: inline-flex; align-items: center; gap: 6px; font-size: 9px; font-weight: 800; letter-spacing: .04em; white-space: nowrap; }
|
|
170
170
|
.chat-runtime-title svg { width: 14px; height: 14px; color: var(--green-dark); }
|
|
171
171
|
.chat-runtime-toolbar label { min-width: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 7px; }
|
|
@@ -176,6 +176,16 @@ h2 { margin: 0; font-size: 19px; line-height: 1.3; }
|
|
|
176
176
|
.chat-message.is-user { margin-left: auto; justify-items: end; }
|
|
177
177
|
.chat-message.is-assistant { margin-right: auto; justify-items: start; }
|
|
178
178
|
.chat-message.has-attachments { width: min(78%, 720px); }
|
|
179
|
+
.chat-progress-group { width: min(78%, 720px); margin: 0 0 22px; color: var(--ink-soft); }
|
|
180
|
+
.chat-progress-group summary { min-height: 32px; padding: 0 3px; list-style: none; display: flex; align-items: center; justify-content: space-between; gap: 14px; cursor: pointer; font-size: 10px; font-weight: 700; }
|
|
181
|
+
.chat-progress-group summary::-webkit-details-marker { display: none; }
|
|
182
|
+
.chat-progress-group summary:focus-visible { outline: 2px solid var(--green); outline-offset: 3px; }
|
|
183
|
+
.progress-summary-title { color: var(--ink); display: inline-flex; align-items: center; gap: 6px; }
|
|
184
|
+
.progress-summary-title svg { width: 14px; height: 14px; color: var(--green-dark); transition: transform 160ms ease; }
|
|
185
|
+
.chat-progress-group[open] .progress-summary-title svg { transform: rotate(90deg); }
|
|
186
|
+
.progress-list { margin: 5px 0 0 8px; padding: 8px 12px 8px 24px; border-left: 2px solid #b9c9c0; font-size: 11px; line-height: 1.6; }
|
|
187
|
+
.progress-list li + li { margin-top: 7px; }
|
|
188
|
+
.progress-list p { margin: 0; }
|
|
179
189
|
.message-meta { padding: 0 3px; color: var(--ink-soft); display: flex; align-items: center; gap: 8px; font-size: 9px; font-weight: 750; }
|
|
180
190
|
.message-meta time { font-weight: 550; }
|
|
181
191
|
.message-bubble { max-width: 100%; padding: 13px 15px; border: 1px solid var(--line); border-radius: 7px; background: white; box-shadow: 0 4px 14px rgb(23 34 29 / 6%); font-size: 13px; line-height: 1.65; overflow-wrap: anywhere; }
|
|
@@ -250,6 +260,14 @@ h2 { margin: 0; font-size: 19px; line-height: 1.3; }
|
|
|
250
260
|
.settings-group h2 { font-size: 14px; }
|
|
251
261
|
.settings-group > label, .field-grid { width: 100%; max-width: 720px; grid-column: 2; }
|
|
252
262
|
.field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
|
|
263
|
+
.toggle-field { min-height: 44px; padding: 0 12px; border: 1px solid var(--line); border-radius: var(--radius); background: #f8faf8; display: flex; align-items: center; justify-content: space-between; gap: 18px; }
|
|
264
|
+
.toggle-control { width: 38px; height: 22px; position: relative; flex: 0 0 38px; }
|
|
265
|
+
.toggle-control input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
|
266
|
+
.toggle-control > span { position: absolute; inset: 0; border: 1px solid #aeb8b2; border-radius: 11px; background: #dfe4e1; transition: background-color .16s ease, border-color .16s ease; }
|
|
267
|
+
.toggle-control > span::after { content: ""; width: 16px; height: 16px; position: absolute; left: 2px; top: 2px; border-radius: 50%; background: white; box-shadow: 0 1px 3px rgb(23 34 29 / 24%); transition: transform .16s ease; }
|
|
268
|
+
.toggle-control input:checked + span { border-color: var(--green-dark); background: var(--green); }
|
|
269
|
+
.toggle-control input:checked + span::after { transform: translateX(16px); }
|
|
270
|
+
.toggle-control input:focus-visible + span { outline: 3px solid color-mix(in srgb, var(--focus) 30%, transparent); outline-offset: 2px; }
|
|
253
271
|
label { min-width: 0; display: grid; gap: 8px; color: var(--ink-soft); font-size: 11px; font-weight: 700; }
|
|
254
272
|
.field-hint { color: var(--ink-soft); font-size: 10px; font-weight: 500; line-height: 1.5; }
|
|
255
273
|
.effective-setting { min-width: 0; display: flex; align-items: center; gap: 7px; }
|