14 mục ưu tiên

Sửa đúng nơi.
Giữ luồng đang có.

14 mục ưu tiên · bản rà soát native v04. Sidebar, trang và component hiện có là nền tảng của mọi thay đổi.

Trạng thái theo source · 26/10/2026

3 mục đã fix trong phạm vi ghi rõ · 4 mục mới có proposal · 5 mục chưa fix · 2 mục chưa xác nhận. “Đã fix” không đồng nghĩa đã xác nhận triển khai trên production; xem phạm vi tại mỗi mục.

G26
Có bản sửa trong proposal

Mời thành viên: giữ lại email bị lỗi

Khôi phục khi mời thành công một phần đã có trong proposal; source sản phẩm vẫn đóng modal khi có ít nhất một lời mời thành công. Lỗi khác domain chưa xác nhận nguyên nhân.

01
Vấn đề

Báo cáo ghi nhận lỗi mời email khác công ty. Trong luồng hiện tại, modal đóng khi chỉ thành công một phần, khiến người dùng mất thông tin địa chỉ lỗi để thử lại.

Giải pháp

Giữ modal khi chỉ thành công một phần; giữ địa chỉ lỗi để thử lại, không gửi lại địa chỉ đã thành công.

Vị trí cập nhật: Cài đặt → Đội ngũ → modal Mời thành viên

Trạng thái và giới hạn

FE đã có bản sửa cô lập. Chưa xác nhận nguyên nhân lỗi email khác công ty; video không chứng minh thư được giao.

Hai video dùng cùng dữ liệu mẫu
BeforeTrang/component hiện có
Tải video Before
AfterTrang/component hiện có
Tải video After

Code diff

InviteMemberModal.tsx

--- a/src/features/brand/components/settings/InviteMemberModal.tsx
+++ b/src/features/brand/components/settings/InviteMemberModal.tsx
@@ -2,9 +2,9 @@
// access-gate-exempt: a child modal whose only entry point is `TeamPanel`'s "Add member"
// button, already gated on `members.manage`. `canGrantOwner` arrives as a prop for the
// same reason: calling `useAccess()` here would duplicate a decision the parent owns.
-import { useId, useMemo, useState } from "react";
+import { useId, useMemo, useRef, useState } from "react";
import { useTranslations } from "next-intl";
import { Mail, Send } from "@/components/icons";
import { Button } from "@/components/ui/button";
@@ -21,9 +21,9 @@
/**
* Invite-members modal. Accepts a pasted list of emails and POSTs one invite per email
* in a loop, not batched. Partial failure is reported: it continues through every email,
- * shows the last error, and only closes when at least one succeeded.
+ * retains failed addresses for retry, and closes only when every address succeeded.
*/
export function InviteMemberModal({
brandId,
canGrantOwner,
@@ -40,35 +40,61 @@
const [emails, setEmails] = useState("");
const [role, setRole] = useState("editor");
const [inviting, setInviting] = useState(false);
const [error, setError] = useState<string | null>(null);
+ const [partial, setPartial] = useState<{
+ created: number;
+ failed: number;
+ } | null>(null);
+ const sendingRef = useRef(false);
const errorId = useId();
const { valid, invalid } = useMemo(() => parseEmails(emails), [emails]);
const formatError = invalid.length
? t("invalidEmail", { emails: invalid.join(", ") })
: null;
async function send() {
+ if (sendingRef.current) {
+ return;
+ }
if (invalid.length) {
setError(t("invalidEmail", { emails: invalid.join(", ") }));
return;
}
if (!valid.length) {
return;
}
setInviting(true);
+ sendingRef.current = true;
setError(null);
+ setPartial(null);
let ok = 0;
+ const failed: { email: string; message: string }[] = [];
for (const email of valid) {
try {
await inviteMember(brandId, email, role);
ok++;
} catch (err) {
- setError(describeError(err, t("inviteFailed", { email })));
+ failed.push({
+ email,
+ message: describeError(err, t("inviteFailed", { email })),
+ });
}
}
setInviting(false);
+ sendingRef.current = false;
+ if (failed.length) {
+ setEmails(failed.map(({ email }) => email).join(", "));
+ setError(
+ failed.map(({ email, message }) => `${email}: ${message}`).join("; ")
+ );
+ setPartial({ created: ok, failed: failed.length });
+ if (ok > 0) {
+ onInvited();
+ }
+ return;
+ }
if (ok > 0) {
toast.success(t("sent", { count: ok }));
onInvited();
onClose();
@@ -106,9 +132,13 @@
aria-label={t("emails")}
aria-invalid={!!formatError}
aria-describedby={formatError || error ? errorId : undefined}
value={emails}
- onChange={(e) => setEmails(e.target.value)}
+ onChange={(e) => {
+ setEmails(e.target.value);
+ setError(null);
+ setPartial(null);
+ }}
onKeyDown={(e) => {
if (e.key === "Enter") {
void send();
}
@@ -124,14 +154,24 @@
</p>
<Select
size="sm"
value={role}
- onChange={setRole}
+ onChange={(next) => {
+ setRole(next);
+ setError(null);
+ setPartial(null);
+ }}
options={roleOptions(canGrantOwner, (r) => tRoles(roleLabelKey(r)))}
/>
{/* The invitee is emailed once, but access comes from signing in with the invited
address, and nothing else on this screen tells the inviter that. */}
<p className="mt-2 text-[11px] text-ink/40">{t("accessHint")}</p>
+
+ {partial && (
+ <p role="status" className="mt-3 text-xs text-ink/70">
+ {t("partialResult", partial)}
+ </p>
+ )}
{(formatError ?? error) && (
// The format check reruns on every keystroke, so it is not a live alert; the
// input points here instead. A failed send still interrupts.
Nhãn giao diện en
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -2006,10 +2006,11 @@
"accessHint": "We'll email them an invite. They'll get access when they sign in with this address.",
"invalidEmail": "Not a valid email: {emails}",
"inviteFailed": "Failed to invite {email}",
- "sent": "{count, plural, =1 {Invitation sent} other {# invitations sent}}",
+ "sent": "{count, plural, one {# invitation created} other {# invitations created}}",
"cancel": "Cancel",
"sending": "Sending…",
- "send": "Send invites"
+ "send": "Send invites",
+ "partialResult": "Created {created} invitation(s); {failed} address(es) still need attention. Only failed addresses remain for retry."
},
"pending": {
"anOwner": "An owner",
Nhãn giao diện vi
--- a/src/i18n/locales/vi.json
+++ b/src/i18n/locales/vi.json
@@ -2006,10 +2006,11 @@
"accessHint": "Chúng tôi sẽ gửi lời mời qua email. Họ sẽ có quyền truy cập khi đăng nhập bằng địa chỉ này.",
"invalidEmail": "Email không hợp lệ: {emails}",
"inviteFailed": "Không mời được {email}",
- "sent": "{count, plural, =1 {Đã gửi lời mời} other {Đã gửi # lời mời}}",
+ "sent": "Đã tạo {count} lời mời",
"cancel": "Hủy",
"sending": "Đang gửi…",
- "send": "Gửi lời mời"
+ "send": "Gửi lời mời",
+ "partialResult": "Đã tạo {created} lời mời; còn {failed} địa chỉ chưa thành công. Chỉ giữ lại địa chỉ lỗi để thử lại."
},
"pending": {
"anOwner": "Một chủ sở hữu",
Tải diff native G26
G18
Có bản sửa trong proposal

Hướng dẫn dùng logo và ảnh mẫu ngay trong guideline

@ và /upload đã có. Hướng dẫn giúp người dùng tìm thấy chúng mới được bổ sung trong proposal, chưa nằm trong source sản phẩm.

02
Vấn đề

Người dùng phải tự nhập guideline và khó biết cách dùng logo, font hoặc ảnh mẫu ngay trong tài liệu.

Giải pháp

Thêm hướng dẫn ngắn về @ và /upload ngay trong tài liệu; không thêm trang hoặc nút điều hướng riêng.

Vị trí cập nhật: Tùy chỉnh → Nguyên tắc thương hiệu → editor hiện có

Trạng thái và giới hạn

FE giúp khám phá chức năng đã có; chưa phải tự trích xuất guideline hoàn chỉnh.

Hai video dùng cùng dữ liệu mẫu
BeforeTrang/component hiện có
Tải video Before
AfterTrang/component hiện có
Tải video After

Code diff

GuidelinesDocument.tsx

--- a/src/features/brand/components/guidelines/GuidelinesDocument.tsx
+++ b/src/features/brand/components/guidelines/GuidelinesDocument.tsx
@@ -162,8 +162,15 @@
<div
ref={editorBoxRef}
className="rounded-2xl border border-hairline bg-canvas px-6 py-5"
>
+ <p className="mb-3 text-xs text-ink/55">
+ {editable
+ ? canAddFiles
+ ? t("helpEditableWithUpload")
+ : t("helpEditableReferences")
+ : t("helpReadOnly")}
+ </p>
<MarkdownMentionEditor
value={value}
onValueChange={onChange}
suggestions={suggestions}
Nhãn giao diện en
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -3692,7 +3692,10 @@
"editorPlaceholder": "Write the brand's rules and judgment here. Type @ to point at a logo, a typeface or an image.",
"emptyLinePlaceholder": "Press / for commands, @ to mention a file…",
"outlineLabel": "Contents",
- "uploadFailed": "Couldn’t upload {name}"
+ "uploadFailed": "Couldn’t upload {name}",
+ "helpEditableWithUpload": "Use @ to reference existing logos, typefaces or example images; use /upload to add a file and insert its reference here.",
+ "helpEditableReferences": "Use @ to reference existing brand files. Adding new files requires upload permission.",
+ "helpReadOnly": "Read-only guidelines. You can still inspect references in this document."
}
},
"landing": {
Nhãn giao diện vi
--- a/src/i18n/locales/vi.json
+++ b/src/i18n/locales/vi.json
@@ -3692,7 +3692,10 @@
"editorPlaceholder": "Viết các quy tắc và nhận định của thương hiệu tại đây. Gõ @ để trỏ tới logo, phông chữ hoặc hình ảnh.",
"emptyLinePlaceholder": "Nhấn / để mở lệnh, @ để nhắc tới tệp…",
"outlineLabel": "Mục lục",
- "uploadFailed": "Không tải lên được {name}"
+ "uploadFailed": "Không tải lên được {name}",
+ "helpEditableWithUpload": "Dùng @ để nhắc logo, font hoặc ảnh mẫu đã có; gõ /upload để thêm tư liệu và chèn tham chiếu ngay tại đây.",
+ "helpEditableReferences": "Dùng @ để nhắc tư liệu đã có. Thêm tư liệu mới cần quyền tải lên.",
+ "helpReadOnly": "Chế độ chỉ đọc. Bạn vẫn có thể xem các tham chiếu trong tài liệu."
}
},
"landing": {
Tải diff native G18
G9
Chưa fix kiểm tra tự động

Kiểm tra trước khi gửi duyệt

Proposal chỉ thêm lời nhắc thủ công; chưa có bộ kiểm tra brand tự động trước khi gửi.

03
Vấn đề

Chưa có bước tự kiểm tra brand trước khi gửi duyệt; sai sản phẩm, logo hoặc nội dung có thể chỉ được phát hiện khi bị trả về.

Giải pháp

Đặt lời nhắc kiểm tra sản phẩm, logo, nội dung, giá và ngày ở bước xác nhận hiện có. Bỏ panel báo cáo giả.

Vị trí cập nhật: Gallery → xem ảnh → Gửi duyệt → xác nhận

Trạng thái và giới hạn

Đây là nhắc kiểm tra thủ công. Kiểm tra brand tự động còn cần BE/model và liên kết đúng phiên bản ảnh/quy tắc.

Hai video dùng cùng dữ liệu mẫu
BeforeTrang/component hiện có
Tải video Before
AfterTrang/component hiện có
Tải video After

Code diff

G4 và G9 dùng chung modal. Diff của file chung hiển thị các thay đổi liên quan của cả hai; không áp dụng chồng hai patch.

SubmitForApprovalModal.tsx

--- a/src/features/artifact/review/components/SubmitForApprovalModal.tsx
+++ b/src/features/artifact/review/components/SubmitForApprovalModal.tsx
@@ -42,8 +42,9 @@
} from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
import { LazyVideoStill } from "@/components/common/lazy-video-still";
import {
Modal,
ModalContent,
@@ -100,8 +101,10 @@
artifactIsAudio?: boolean;
/** When present the modal edits this open request with a PATCH instead of creating one. */
editing?: {
approvalId: string;
+ caption?: string | null;
+ note?: string | null;
approverIds: string[]; // already ordered by stage
approvalMode: "parallel" | "sequential";
requiredApprovals: number;
};
@@ -120,8 +123,11 @@
// Lazy initializers: `editing` only seeds state on mount, never re-applies on rerender.
const [approverIds, setApproverIds] = useState<string[]>(
() => editing?.approverIds ?? []
);
+ const [caption, setCaption] = useState(() => editing?.caption ?? "");
+ const [senderNote, setSenderNote] = useState(() => editing?.note ?? "");
+ const contextId = useId();
const [search, setSearch] = useState("");
/** How many of those asked must say yes; `null` means everyone, and is the default. */
const [quorum, setQuorum] = useState<number | null>(() => {
if (!editing) return null;
@@ -343,8 +349,16 @@
const submit = useMutation({
meta: { handlesOwnError: true },
mutationFn: async (): Promise<{ failed: number }> => {
const input = {
+ ...(caption.trim() ||
+ (editing?.caption && caption.trim() !== editing.caption.trim())
+ ? { caption: caption.trim() }
+ : {}),
+ ...(senderNote.trim() ||
+ (editing?.note && senderNote.trim() !== editing.note.trim())
+ ? { note: senderNote.trim() }
+ : {}),
approverIds,
approvalMode: mode,
// The server forces `requiredApprovals` to the panel size for sequential,
// so it is only sent for parallel.
@@ -717,8 +731,43 @@
</div>
)
);
+ const contextFields = (
+ <section className="flex flex-col gap-3">
+ <Heading level={3} size="panel">
+ {t("contextTitle")}
+ </Heading>
+ <p className="text-xs text-ink/60">{t("contextHint")}</p>
+ <div className="flex flex-col gap-1.5">
+ <Label htmlFor={`${contextId}-caption`} size="xs">
+ {t("captionLabel")}
+ </Label>
+ <Textarea
+ id={`${contextId}-caption`}
+ value={caption}
+ onChange={(event) => setCaption(event.target.value)}
+ rows={3}
+ maxLength={5000}
+ variant="filled"
+ />
+ </div>
+ <div className="flex flex-col gap-1.5">
+ <Label htmlFor={`${contextId}-note`} size="xs">
+ {t("senderNoteLabel")}
+ </Label>
+ <Textarea
+ id={`${contextId}-note`}
+ value={senderNote}
+ onChange={(event) => setSenderNote(event.target.value)}
+ rows={2}
+ maxLength={2000}
+ variant="filled"
+ />
+ </div>
+ </section>
+ );
+
return (
<Modal open onOpenChange={(next) => !next && onClose()}>
{/* Overrides the base `grid` so a long panel scrolls its body and header and footer stay on screen. */}
<ModalContent
@@ -782,8 +831,29 @@
) : step === "confirm" ? (
<ScrollArea className="min-h-0 flex-1">
<div className="flex flex-col gap-4">
{thumbnail}
+ {(caption.trim() || senderNote.trim()) && (
+ <section className="flex flex-col gap-2 rounded-xl bg-surface-soft p-3">
+ {caption.trim() && (
+ <div>
+ <Label size="xs">{t("captionLabel")}</Label>
+ <p className="text-sm break-words whitespace-pre-wrap">
+ {caption.trim()}
+ </p>
+ </div>
+ )}
+ {senderNote.trim() && (
+ <div>
+ <Label size="xs">{t("senderNoteLabel")}</Label>
+ <p className="text-sm break-words whitespace-pre-wrap">
+ {senderNote.trim()}
+ </p>
+ </div>
+ )}
+ </section>
+ )}
+ <p className="text-xs text-ink/60">{t("preflightReminder")}</p>
<section>
<Label
size="xs"
@@ -838,8 +908,9 @@
<Heading level={3} size="panel">
{t("submission")}
</Heading>
{thumbnail}
+ {contextFields}
</section>
<section ref={pickerRef} className="flex flex-col gap-4">
<Heading level={3} size="panel">
Nhãn giao diện en
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -135,7 +135,10 @@
"resubmitDescription": "Notify the approvers again. Keep the approvals already given, or clear them for a fresh round.",
"cancel": "Cancel",
"keepApprovals": "Keep existing approvals",
- "startFresh": "Ask everyone again"
+ "startFresh": "Ask everyone again",
+ "contextTitle": "Submitted context",
+ "captionLabel": "Post copy",
+ "senderNoteLabel": "Note from submitter"
},
"submit": {
"unnamedMember": "Unnamed member",
@@ -188,7 +191,12 @@
"cancel": "Cancel",
"saveChanges": "Save changes",
"sendForApproval": "Send for approval",
- "dragToReorder": "Drag to reorder"
+ "dragToReorder": "Drag to reorder",
+ "contextTitle": "Review context",
+ "contextHint": "Optional. Add the post copy and what the reviewer should check. For several items, this context is shared.",
+ "captionLabel": "Post copy",
+ "senderNoteLabel": "Note for reviewer",
+ "preflightReminder": "Before sending, review the product, logo, wording, prices and dates in this creative."
}
},
"artifact": {
Nhãn giao diện vi
--- a/src/i18n/locales/vi.json
+++ b/src/i18n/locales/vi.json
@@ -135,7 +135,10 @@
"resubmitDescription": "Thông báo lại cho người duyệt. Giữ các lượt duyệt đã có, hoặc xóa chúng để bắt đầu vòng mới.",
"cancel": "Hủy",
"keepApprovals": "Giữ các lượt đã duyệt",
- "startFresh": "Duyệt lại từ đầu"
+ "startFresh": "Duyệt lại từ đầu",
+ "contextTitle": "Ngữ cảnh đã gửi",
+ "captionLabel": "Nội dung đăng",
+ "senderNoteLabel": "Ghi chú của người gửi"
},
"submit": {
"unnamedMember": "Thành viên chưa có tên",
@@ -188,7 +191,12 @@
"cancel": "Hủy",
"saveChanges": "Lưu thay đổi",
"sendForApproval": "Gửi để phê duyệt",
- "dragToReorder": "Kéo để sắp xếp lại"
+ "dragToReorder": "Kéo để sắp xếp lại",
+ "contextTitle": "Ngữ cảnh gửi duyệt",
+ "contextHint": "Không bắt buộc. Thêm nội dung đăng và điều cần người duyệt kiểm tra. Nếu chọn nhiều mục, thông tin này được dùng chung.",
+ "captionLabel": "Nội dung đăng",
+ "senderNoteLabel": "Ghi chú cho người duyệt",
+ "preflightReminder": "Trước khi gửi, kiểm tra lại sản phẩm, logo, nội dung, giá và ngày trên thiết kế này."
}
},
"artifact": {
Tải diff native G9
G25
Chưa xác nhận đã fix

Dùng ảnh tham khảo trong composer hiện có

Luồng đính kèm đã có, nhưng chưa có thử nghiệm model chứng minh hiểu ảnh tham khảo và ý tưởng phức tạp tốt hơn.

04
Vấn đề

Trợ lý chưa hiểu ổn định ảnh tham khảo và yêu cầu phức tạp, khiến kết quả lệch ý định của người dùng.

Giải pháp

Bỏ form Reference intent và API proposal riêng. Giữ upload, preview, xóa ảnh và prompt trong composer gốc.

Vị trí cập nhật: Trang chủ → composer → tệp/ảnh đính kèm

Trạng thái và giới hạn

Chưa có bản sửa chất lượng hiểu ảnh. Vai trò ảnh bổ sung cần contract BE; mức tuân thủ cần thử nghiệm model.

Video cũ đã được rút khỏi bản review này.

Chưa có video native mới đủ điều kiện cho mục này. Không hiển thị video dựng sai trang hoặc mô phỏng phụ thuộc chưa triển khai.

Code diff

StudioComposer.tsx: giữ nguyên source hiện tại. Chưa có bản vá mới cho mục này.

useComposerAttachments.ts: giữ nguyên source hiện tại. Chưa có bản vá mới cho mục này.

G27
Có bản sửa trong proposal

Nhận biết câu hỏi AI đang chờ trả lời

Nhãn chú ý câu hỏi trên dock đã làm trong proposal; source sản phẩm chưa nhận thay đổi này.

05
Vấn đề

AI hỏi thêm nhưng câu hỏi có thể bị bỏ lỡ khi khung chat đóng; người dùng không biết cần trả lời để tiếp tục.

Giải pháp

Đổi nhãn và thêm dấu chú ý trên chính nút mở chat khi có câu hỏi thật đang chờ; mở lại drawer hiện có.

Vị trí cập nhật: Trang chủ → nút mở chat trên dock → ClarifyPanel hiện có

Trạng thái và giới hạn

FE đã có bản sửa cô lập; không thêm form câu hỏi hoặc endpoint tiếp tục riêng. Chất lượng câu hỏi là phần agent/model.

Hai video dùng cùng dữ liệu mẫu
BeforeTrang/component hiện có
Tải video Before
AfterTrang/component hiện có
Tải video After

Code diff

ComposerDock.tsx

--- a/src/features/artifact/components/chat/ComposerDock.tsx
+++ b/src/features/artifact/components/chat/ComposerDock.tsx
@@ -22,8 +22,9 @@
onExpand,
children,
contentRef,
inline = false,
+ needsClarification = false,
}: {
visible: boolean;
onExpand: () => void;
children: ReactNode;
@@ -31,8 +32,10 @@
contentRef?: (node: HTMLDivElement | null) => void;
/** Sits in the page flow (Home's Feed, above Trending) instead of floating at the
* bottom; the layout then reserves no space for it. */
inline?: boolean;
+ /** The active editable session has a settled question awaiting an answer. */
+ needsClarification?: boolean;
}) {
const t = useTranslations("artifactChat.dock");
return (
<AnimatePresence>
@@ -69,9 +72,19 @@
onClick={onExpand}
className="self-end shadow-sm"
>
<MessagesSquare className="size-3.5" />
- {t("openAsPanel")}
+ {needsClarification && (
+ <span
+ aria-hidden="true"
+ className="size-1.5 rounded-full bg-warning"
+ />
+ )}
+ <span aria-live="polite">
+ {needsClarification
+ ? t("answerPendingQuestion")
+ : t("openAsPanel")}
+ </span>
</Button>
{children}
</div>
</m.div>

StudioClient.tsx

--- a/src/features/artifact/components/studio/StudioClient.tsx
+++ b/src/features/artifact/components/studio/StudioClient.tsx
@@ -940,8 +940,11 @@
// The dock yields the screen bottom to `BulkBar`, which would overlap it.
<ComposerDock
visible={!chatOpen && !folderOrg.selection.selectionActive}
onExpand={openChat}
+ needsClarification={
+ !replaying && !chat.viewOnly && chat.currentClarify !== null
+ }
contentRef={measureDock}
inline={inline}
>
{dockComposer}
Nhãn giao diện en
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -345,7 +345,8 @@
"resizeAria": "Resize chat panel (arrow keys to adjust, Home to reset)"
},
"dock": {
- "openAsPanel": "Open chat"
+ "openAsPanel": "Open chat",
+ "answerPendingQuestion": "Answer pending question"
},
"media": {
"noCaptions": "No captions available",
Nhãn giao diện vi
--- a/src/i18n/locales/vi.json
+++ b/src/i18n/locales/vi.json
@@ -345,7 +345,8 @@
"resizeAria": "Đổi kích thước bảng chat (phím mũi tên để điều chỉnh, Home để đặt lại)"
},
"dock": {
- "openAsPanel": "Mở khung chat"
+ "openAsPanel": "Mở khung chat",
+ "answerPendingQuestion": "Trả lời câu hỏi đang chờ"
},
"media": {
"noCaptions": "Không có phụ đề",
Tải diff native G27
G28
Đã fix · tải nhiều ảnh đã chọn

Tải nhiều ảnh qua thanh chọn sẵn có

Source đã có chọn nhiều → hộp thoại tải chung. Chưa xác nhận tự gom toàn bộ size của cùng thiết kế hoặc tải ZIP thành công qua hệ thống thật.

06
Vấn đề

Người dùng cần tải cả bộ ảnh hoặc size trong một lần. Chọn nhiều và tải chung đã có, nhưng tự gom toàn bộ size của cùng thiết kế chưa được xác nhận.

Giải pháp

Dùng đúng chọn nhiều và FolderDownloadModal đã có; bỏ nút tải bộ ảnh dựng ở trang riêng.

Vị trí cập nhật: Gallery → chọn nhiều ấn phẩm → BulkBar → hộp thoại tải

Trạng thái và giới hạn

Chức năng chọn nhiều/tải chung đã có trong source. Tự gom toàn bộ size của một họ thiết kế cần metadata phù hợp.

Hiện trạng trên trang có sẵn · không dựng Before lỗi giả

Code diff

BulkBar.tsx: giữ nguyên source hiện tại. Chưa có bản vá mới cho mục này.

FolderDownloadModal.tsx: giữ nguyên source hiện tại. Chưa có bản vá mới cho mục này.

G29
Chưa fix kiểm tra từng size

Kiểm tra chất lượng từng size

Chưa có kiểm tra tự động hay trạng thái chất lượng theo từng revision đáng tin cậy.

07
Vấn đề

Người dùng phải kiểm tra chữ, logo và sản phẩm trên từng size; chưa có trạng thái kiểm tra tự động đáng tin cậy cho từng phiên bản.

Giải pháp

Bỏ checklist nổi và trạng thái đã kiểm tra lưu cục bộ. Kiểm tra thực tế trong viewer; chỉ thêm trạng thái vào nơi này khi có dữ liệu tin cậy.

Vị trí cập nhật: Gallery → ảnh và các phiên bản → viewer hiện có

Trạng thái và giới hạn

Chưa triển khai đánh giá logo/chữ/sản phẩm tự động. Cần quan hệ giữa các size, trạng thái theo revision và BE/model.

Video cũ đã được rút khỏi bản review này.

Chưa có video native mới đủ điều kiện cho mục này. Không hiển thị video dựng sai trang hoặc mô phỏng phụ thuộc chưa triển khai.

Code diff

ArtifactLightbox.tsx: giữ nguyên source hiện tại. Chưa có bản vá mới cho mục này.

G3
Chưa fix thông báo email

Thông báo email cho người duyệt

Phần approval được kiểm tra chưa có luồng worker/email gắn với sự kiện yêu cầu duyệt.

08
Vấn đề

Người duyệt không nhận được thông báo khi có yêu cầu mới, khiến người tạo phải nhắc qua kênh khác.

Giải pháp

Giữ FE và trang Phê duyệt. BE phát sự kiện, lưu tác vụ gửi bền vững, worker gửi email có liên kết về yêu cầu.

Vị trí cập nhật: Gửi duyệt hiện có → email → trang Phê duyệt hiện có

Trạng thái và giới hạn

Chưa triển khai BE gửi email trong bản này. Không dùng hộp thư giả để minh họa là đã giao thư.

Video cũ đã được rút khỏi bản review này.

Chưa có video native mới đủ điều kiện cho mục này. Không hiển thị video dựng sai trang hoặc mô phỏng phụ thuộc chưa triển khai.

Code diff

approval.service.ts

--- a/src/features/artifact/review/services/approval.service.ts
+++ b/src/features/artifact/review/services/approval.service.ts
@@ -40,8 +40,9 @@
id: z.string(),
brandId: z.string(),
status: z.nativeEnum(ApprovalStatus),
caption: z.string().nullable(),
+ note: z.string().nullable().optional(),
requiredApprovals: z.number(),
approvedCount: z.number(),
submittedAt: z.string(),
decidedAt: z.string().nullable(),
@@ -123,8 +124,9 @@
artifactId: string,
input: {
approverIds: string[];
caption?: string;
+ note?: string;
requiredApprovals?: number;
approvalMode?: "sequential" | "parallel";
}
): Promise<Approval> => {
@@ -145,8 +147,9 @@
brandId: string,
approvalId: string,
input: {
caption?: string;
+ note?: string;
approverIds?: string[];
requiredApprovals?: number;
approvalMode?: "sequential" | "parallel";
}
Tải diff native G3
G4
Có bản sửa trong proposal

Gói duyệt có caption và ghi chú

BE đã hỗ trợ caption/note. FE nhập và hiển thị ngữ cảnh mới nằm trong proposal; kênh/link/snapshot còn thiếu.

09
Vấn đề

Gói duyệt thiếu ngữ cảnh như caption, ghi chú, kênh và link, khiến người duyệt phải hỏi lại.

Giải pháp

Thêm caption/ghi chú ngay trong modal gốc, bước xác nhận và panel duyệt; dùng trường API đang được BE hỗ trợ.

Vị trí cập nhật: Modal gửi duyệt của người tạo; ReviewPanel trong trang Phê duyệt của người duyệt

Trạng thái và giới hạn

FE đã có bản sửa cô lập. Kênh, link đích và snapshot bất biến chưa được hai trường này giải quyết.

Hai video dùng cùng dữ liệu mẫu
BeforeTrang/component hiện có
Tải video Before
AfterTrang/component hiện có
Tải video After

Code diff

G4 và G9 dùng chung modal. Diff của file chung hiển thị các thay đổi liên quan của cả hai; không áp dụng chồng hai patch.

SubmitForApprovalModal.tsx

--- a/src/features/artifact/review/components/SubmitForApprovalModal.tsx
+++ b/src/features/artifact/review/components/SubmitForApprovalModal.tsx
@@ -42,8 +42,9 @@
} from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
import { LazyVideoStill } from "@/components/common/lazy-video-still";
import {
Modal,
ModalContent,
@@ -100,8 +101,10 @@
artifactIsAudio?: boolean;
/** When present the modal edits this open request with a PATCH instead of creating one. */
editing?: {
approvalId: string;
+ caption?: string | null;
+ note?: string | null;
approverIds: string[]; // already ordered by stage
approvalMode: "parallel" | "sequential";
requiredApprovals: number;
};
@@ -120,8 +123,11 @@
// Lazy initializers: `editing` only seeds state on mount, never re-applies on rerender.
const [approverIds, setApproverIds] = useState<string[]>(
() => editing?.approverIds ?? []
);
+ const [caption, setCaption] = useState(() => editing?.caption ?? "");
+ const [senderNote, setSenderNote] = useState(() => editing?.note ?? "");
+ const contextId = useId();
const [search, setSearch] = useState("");
/** How many of those asked must say yes; `null` means everyone, and is the default. */
const [quorum, setQuorum] = useState<number | null>(() => {
if (!editing) return null;
@@ -343,8 +349,16 @@
const submit = useMutation({
meta: { handlesOwnError: true },
mutationFn: async (): Promise<{ failed: number }> => {
const input = {
+ ...(caption.trim() ||
+ (editing?.caption && caption.trim() !== editing.caption.trim())
+ ? { caption: caption.trim() }
+ : {}),
+ ...(senderNote.trim() ||
+ (editing?.note && senderNote.trim() !== editing.note.trim())
+ ? { note: senderNote.trim() }
+ : {}),
approverIds,
approvalMode: mode,
// The server forces `requiredApprovals` to the panel size for sequential,
// so it is only sent for parallel.
@@ -717,8 +731,43 @@
</div>
)
);
+ const contextFields = (
+ <section className="flex flex-col gap-3">
+ <Heading level={3} size="panel">
+ {t("contextTitle")}
+ </Heading>
+ <p className="text-xs text-ink/60">{t("contextHint")}</p>
+ <div className="flex flex-col gap-1.5">
+ <Label htmlFor={`${contextId}-caption`} size="xs">
+ {t("captionLabel")}
+ </Label>
+ <Textarea
+ id={`${contextId}-caption`}
+ value={caption}
+ onChange={(event) => setCaption(event.target.value)}
+ rows={3}
+ maxLength={5000}
+ variant="filled"
+ />
+ </div>
+ <div className="flex flex-col gap-1.5">
+ <Label htmlFor={`${contextId}-note`} size="xs">
+ {t("senderNoteLabel")}
+ </Label>
+ <Textarea
+ id={`${contextId}-note`}
+ value={senderNote}
+ onChange={(event) => setSenderNote(event.target.value)}
+ rows={2}
+ maxLength={2000}
+ variant="filled"
+ />
+ </div>
+ </section>
+ );
+
return (
<Modal open onOpenChange={(next) => !next && onClose()}>
{/* Overrides the base `grid` so a long panel scrolls its body and header and footer stay on screen. */}
<ModalContent
@@ -782,8 +831,29 @@
) : step === "confirm" ? (
<ScrollArea className="min-h-0 flex-1">
<div className="flex flex-col gap-4">
{thumbnail}
+ {(caption.trim() || senderNote.trim()) && (
+ <section className="flex flex-col gap-2 rounded-xl bg-surface-soft p-3">
+ {caption.trim() && (
+ <div>
+ <Label size="xs">{t("captionLabel")}</Label>
+ <p className="text-sm break-words whitespace-pre-wrap">
+ {caption.trim()}
+ </p>
+ </div>
+ )}
+ {senderNote.trim() && (
+ <div>
+ <Label size="xs">{t("senderNoteLabel")}</Label>
+ <p className="text-sm break-words whitespace-pre-wrap">
+ {senderNote.trim()}
+ </p>
+ </div>
+ )}
+ </section>
+ )}
+ <p className="text-xs text-ink/60">{t("preflightReminder")}</p>
<section>
<Label
size="xs"
@@ -838,8 +908,9 @@
<Heading level={3} size="panel">
{t("submission")}
</Heading>
{thumbnail}
+ {contextFields}
</section>
<section ref={pickerRef} className="flex flex-col gap-4">
<Heading level={3} size="panel">

ReviewPanel.tsx

--- a/src/features/artifact/review/components/ReviewPanel.tsx
+++ b/src/features/artifact/review/components/ReviewPanel.tsx
@@ -194,8 +194,35 @@
return (
<MentionResolutionProvider brandId={brandId}>
<div className="flex min-h-0 flex-1 flex-col">
+ {approval && (approval.caption?.trim() || approval.note?.trim()) && (
+ <section
+ aria-label={t("contextTitle")}
+ className="flex shrink-0 flex-col gap-2 border-b border-hairline px-3.5 py-3"
+ >
+ {approval.caption?.trim() && (
+ <div>
+ <p className="text-xs font-semibold text-ink/60">
+ {t("captionLabel")}
+ </p>
+ <p className="max-h-36 overflow-y-auto text-sm break-words whitespace-pre-wrap">
+ {approval.caption}
+ </p>
+ </div>
+ )}
+ {approval.note?.trim() && (
+ <div>
+ <p className="text-xs font-semibold text-ink/60">
+ {t("senderNoteLabel")}
+ </p>
+ <p className="max-h-28 overflow-y-auto text-sm break-words whitespace-pre-wrap">
+ {approval.note}
+ </p>
+ </div>
+ )}
+ </section>
+ )}
{/* Untitled and with nothing to show, the header would be an empty band. */}
{(showTitle || canClose || canReopen || approval) && (
<header className="flex shrink-0 flex-col gap-y-1 px-3.5 py-2.5">
<div className="flex items-center justify-between gap-x-2 empty:hidden">
@@ -364,8 +391,10 @@
editing={
editingOpen && approval
? {
approvalId: approval.id,
+ caption: approval.caption,
+ note: approval.note,
approverIds: orderedApproverIds,
approvalMode: approval.approvalMode,
requiredApprovals: approval.requiredApprovals,
}

approval.service.ts

--- a/src/features/artifact/review/services/approval.service.ts
+++ b/src/features/artifact/review/services/approval.service.ts
@@ -40,8 +40,9 @@
id: z.string(),
brandId: z.string(),
status: z.nativeEnum(ApprovalStatus),
caption: z.string().nullable(),
+ note: z.string().nullable().optional(),
requiredApprovals: z.number(),
approvedCount: z.number(),
submittedAt: z.string(),
decidedAt: z.string().nullable(),
@@ -123,8 +124,9 @@
artifactId: string,
input: {
approverIds: string[];
caption?: string;
+ note?: string;
requiredApprovals?: number;
approvalMode?: "sequential" | "parallel";
}
): Promise<Approval> => {
@@ -145,8 +147,9 @@
brandId: string,
approvalId: string,
input: {
caption?: string;
+ note?: string;
approverIds?: string[];
requiredApprovals?: number;
approvalMode?: "sequential" | "parallel";
}

types.ts

--- a/src/features/artifact/review/types.ts
+++ b/src/features/artifact/review/types.ts
@@ -50,8 +50,10 @@
id: string;
brandId: string;
status: ApprovalStatus;
caption: string | null;
+ /** Submitter guidance, separate from a reviewer decision note. */
+ note?: string | null;
/** How many approvals the CURRENT open stage needs, and how many it has — not the whole panel. */
requiredApprovals: number;
approvedCount: number;
submittedAt: string;
Nhãn giao diện en
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -135,7 +135,10 @@
"resubmitDescription": "Notify the approvers again. Keep the approvals already given, or clear them for a fresh round.",
"cancel": "Cancel",
"keepApprovals": "Keep existing approvals",
- "startFresh": "Ask everyone again"
+ "startFresh": "Ask everyone again",
+ "contextTitle": "Submitted context",
+ "captionLabel": "Post copy",
+ "senderNoteLabel": "Note from submitter"
},
"submit": {
"unnamedMember": "Unnamed member",
@@ -188,7 +191,12 @@
"cancel": "Cancel",
"saveChanges": "Save changes",
"sendForApproval": "Send for approval",
- "dragToReorder": "Drag to reorder"
+ "dragToReorder": "Drag to reorder",
+ "contextTitle": "Review context",
+ "contextHint": "Optional. Add the post copy and what the reviewer should check. For several items, this context is shared.",
+ "captionLabel": "Post copy",
+ "senderNoteLabel": "Note for reviewer",
+ "preflightReminder": "Before sending, review the product, logo, wording, prices and dates in this creative."
}
},
"artifact": {
Nhãn giao diện vi
--- a/src/i18n/locales/vi.json
+++ b/src/i18n/locales/vi.json
@@ -135,7 +135,10 @@
"resubmitDescription": "Thông báo lại cho người duyệt. Giữ các lượt duyệt đã có, hoặc xóa chúng để bắt đầu vòng mới.",
"cancel": "Hủy",
"keepApprovals": "Giữ các lượt đã duyệt",
- "startFresh": "Duyệt lại từ đầu"
+ "startFresh": "Duyệt lại từ đầu",
+ "contextTitle": "Ngữ cảnh đã gửi",
+ "captionLabel": "Nội dung đăng",
+ "senderNoteLabel": "Ghi chú của người gửi"
},
"submit": {
"unnamedMember": "Thành viên chưa có tên",
@@ -188,7 +191,12 @@
"cancel": "Hủy",
"saveChanges": "Lưu thay đổi",
"sendForApproval": "Gửi để phê duyệt",
- "dragToReorder": "Kéo để sắp xếp lại"
+ "dragToReorder": "Kéo để sắp xếp lại",
+ "contextTitle": "Ngữ cảnh gửi duyệt",
+ "contextHint": "Không bắt buộc. Thêm nội dung đăng và điều cần người duyệt kiểm tra. Nếu chọn nhiều mục, thông tin này được dùng chung.",
+ "captionLabel": "Nội dung đăng",
+ "senderNoteLabel": "Ghi chú cho người duyệt",
+ "preflightReminder": "Trước khi gửi, kiểm tra lại sản phẩm, logo, nội dung, giá và ngày trên thiết kế này."
}
},
"artifact": {
Tải diff native G4
G32
Chưa fix duyệt ngoài workspace

Người ngoài workspace xem yêu cầu duyệt

Quyền duyệt hiện tại yêu cầu thành viên; chưa có quyền khách giới hạn cho ấn phẩm và media.

10
Vấn đề

Người duyệt ngoài workspace không xem được ấn phẩm qua liên kết hiện tại vì quyền truy cập yêu cầu thành viên.

Giải pháp

Giữ kiểm tra thành viên. Dùng mời vào workspace nếu phù hợp; duyệt với khách ngoài cần quyền giới hạn do BE cấp.

Vị trí cập nhật: Trang Phê duyệt và kiểm tra quyền hiện có

Trạng thái và giới hạn

Chưa có guest review hoàn chỉnh. Không bỏ guard hoặc tạo quyền ở FE; cần xác thực, phạm vi ảnh/media, hết hạn và thu hồi.

Video cũ đã được rút khỏi bản review này.

Chưa có video native mới đủ điều kiện cho mục này. Không hiển thị video dựng sai trang hoặc mô phỏng phụ thuộc chưa triển khai.

Code diff

ReviewPanel.tsx

--- a/src/features/artifact/review/components/ReviewPanel.tsx
+++ b/src/features/artifact/review/components/ReviewPanel.tsx
@@ -194,8 +194,35 @@
return (
<MentionResolutionProvider brandId={brandId}>
<div className="flex min-h-0 flex-1 flex-col">
+ {approval && (approval.caption?.trim() || approval.note?.trim()) && (
+ <section
+ aria-label={t("contextTitle")}
+ className="flex shrink-0 flex-col gap-2 border-b border-hairline px-3.5 py-3"
+ >
+ {approval.caption?.trim() && (
+ <div>
+ <p className="text-xs font-semibold text-ink/60">
+ {t("captionLabel")}
+ </p>
+ <p className="max-h-36 overflow-y-auto text-sm break-words whitespace-pre-wrap">
+ {approval.caption}
+ </p>
+ </div>
+ )}
+ {approval.note?.trim() && (
+ <div>
+ <p className="text-xs font-semibold text-ink/60">
+ {t("senderNoteLabel")}
+ </p>
+ <p className="max-h-28 overflow-y-auto text-sm break-words whitespace-pre-wrap">
+ {approval.note}
+ </p>
+ </div>
+ )}
+ </section>
+ )}
{/* Untitled and with nothing to show, the header would be an empty band. */}
{(showTitle || canClose || canReopen || approval) && (
<header className="flex shrink-0 flex-col gap-y-1 px-3.5 py-2.5">
<div className="flex items-center justify-between gap-x-2 empty:hidden">
@@ -364,8 +391,10 @@
editing={
editingOpen && approval
? {
approvalId: approval.id,
+ caption: approval.caption,
+ note: approval.note,
approverIds: orderedApproverIds,
approvalMode: approval.approvalMode,
requiredApprovals: approval.requiredApprovals,
}
Tải diff native G32
G7
Đã fix · encoder giữ PNG/JPG

Giữ đúng PNG/JPG khi xuất

Đã xuất file thực xác nhận định dạng. Câu hướng dẫn FE cũ còn sai; phần đồng bộ câu này mới nằm trong proposal. Chưa xác nhận toàn bộ HTTP/ZIP.

11
Vấn đề

Encoder đã giữ PNG/JPG, nhưng hướng dẫn trong hộp thoại tải nhiều ảnh vẫn nói PNG quá nặng sẽ chuyển thành JPG, gây hiểu sai.

Giải pháp

Sửa câu hướng dẫn còn nói PNG sẽ chuyển sang JPG, ngay trong hộp thoại tải nhiều ảnh. Encoder giữ nguyên vì đã bảo toàn định dạng.

Vị trí cập nhật: Gallery → chọn nhiều → hộp thoại tải hiện có; encoder giữ nguyên

Trạng thái và giới hạn

Video chỉ so sánh câu hướng dẫn trong hộp thoại native. Bốn file thật đã kiểm tra định dạng và 900×600. Chưa kiểm tra toàn bộ HTTP/auth/ZIP; dung lượng mục tiêu không luôn đạt.

Hai video dùng cùng dữ liệu mẫu
BeforeTrang/component hiện có
Tải video Before
AfterTrang/component hiện có
Tải video After

Code diff

Nhãn giao diện en
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -608,7 +608,7 @@
"disclaimerText": "Disclaimer text",
"resetOverride": "Reset this image to the shared look",
"cannotResize": "This file type can't be resized or labeled — it ships in the zip as-is.",
- "limitsHint": "Images over the limit are compressed; a PNG that is still too heavy is saved as JPG.",
+ "limitsHint": "Images are compressed toward the chosen size. The selected PNG or JPG format is preserved; the final file may exceed the size target.",
"limitInvalid": "A size limit must be at least 1 KB — switch back to No limit to leave images uncapped.",
"downloadCsv": "Download CSV",
"preparingButton": "Preparing…",
Nhãn giao diện vi
--- a/src/i18n/locales/vi.json
+++ b/src/i18n/locales/vi.json
@@ -608,7 +608,7 @@
"disclaimerText": "Nội dung miễn trừ",
"resetOverride": "Đặt lại ảnh này về giao diện chung",
"cannotResize": "Loại tệp này không thể đổi kích thước hay gắn nhãn — tệp được đưa vào zip nguyên trạng.",
- "limitsHint": "Ảnh vượt dung lượng sẽ được nén; PNG vẫn quá nặng sẽ được lưu thành JPG.",
+ "limitsHint": "Ảnh được nén theo mức dung lượng đã chọn và giữ định dạng PNG hoặc JPG. Tệp cuối có thể vẫn lớn hơn mức dung lượng mục tiêu.",
"limitInvalid": "Giới hạn dung lượng phải ít nhất 1 KB — chuyển lại Không giới hạn để không giới hạn ảnh.",
"downloadCsv": "Tải CSV",
"preparingButton": "Đang chuẩn bị…",
Tải diff native G7

Chỉ đổi nhãn giao diện; không đổi encoder. File thực: g7-current-png-unlimited.png · g7-current-png-51200.png · g7-current-jpg-unlimited.jpg · g7-current-jpg-51200.jpg ·

G15
Đã fix · tạo Playbook trực tiếp

Tạo Playbook không cần ZIP

Source đã có tạo nháp và lưu trực tiếp qua editor, không bắt buộc chuẩn bị ZIP. Import ZIP vẫn là lựa chọn riêng.

12
Vấn đề

Báo cáo ghi nhận tạo Playbook phải chuẩn bị file và ZIP. Source hiện tại đã có tạo trực tiếp; cần thể hiện đúng luồng này và giữ an toàn bản nháp.

Giải pháp

Giữ thao tác tạo nháp sẵn có, một bộ tên/nội dung, file hỗ trợ và bảo vệ nháp. Bỏ form hướng dẫn trùng.

Vị trí cập nhật: Trang Playbook → Playbook mới → editor hiện có

Trạng thái và giới hạn

Tạo trực tiếp đã có trong source hiện tại. Không trình bày chức năng này thành một bản sửa mới.

Hiện trạng trên trang có sẵn · không dựng Before lỗi giả

Code diff

PlaybooksWorkbench.tsx: giữ nguyên source hiện tại. Chưa có bản vá mới cho mục này.

PlaybookEditor.tsx: giữ nguyên source hiện tại. Chưa có bản vá mới cho mục này.

G31
Chưa xác nhận đã fix

Playbook tạo kết quả lặp và ảnh phẳng

Chưa có kết quả đánh giá đầu ra nhiều lượt chứng minh giảm ảnh phẳng hoặc lặp bố cục.

13
Vấn đề

Playbook có thể tạo ảnh phẳng hoặc lặp bố cục; bật/tắt trên giao diện không giải quyết được chất lượng đầu ra.

Giải pháp

Bỏ toggle và panel đánh giá bổ sung. Dùng nội dung, bật/tắt và lịch sử có sẵn; cải thiện recipe và cách agent áp dụng.

Vị trí cập nhật: Thư viện/editor Playbook và kết quả chạy hiện có

Trạng thái và giới hạn

Chưa có bằng chứng chất lượng tăng. Cần trace recipe/model thực và đánh giá nhiều lượt cùng brief; không thể fix bằng một nút UI.

Video cũ đã được rút khỏi bản review này.

Chưa có video native mới đủ điều kiện cho mục này. Không hiển thị video dựng sai trang hoặc mô phỏng phụ thuộc chưa triển khai.

Code diff

PlaybooksWorkbench.tsx: giữ nguyên source hiện tại. Chưa có bản vá mới cho mục này.

G13
Chưa fix đổi tên chat

Đổi tên chat từ lịch sử phiên

Controller chat hiện chưa có thao tác cập nhật tên; đổi tên workflow không giải quyết mục này.

14
Vấn đề

Người dùng chưa đổi được tên chat để dễ tìm lại; thao tác đổi tên workflow hiện có không áp dụng cho chat.

Giải pháp

Đặt đổi tên trong menu dòng phiên sau khi BE có thao tác đổi tên chat; bỏ nút toàn cục và API proposal giả.

Vị trí cập nhật: Trang chủ → lịch sử phiên/SessionTabs hiện có

Trạng thái và giới hạn

BE hiện chưa có endpoint đổi tên chat. API đổi tên workflow không thay thế cho chat. Chưa dựng video thành lưu tên thành công.

Video cũ đã được rút khỏi bản review này.

Chưa có video native mới đủ điều kiện cho mục này. Không hiển thị video dựng sai trang hoặc mô phỏng phụ thuộc chưa triển khai.

Code diff

ExecutionHistoryRail.tsx: giữ nguyên source hiện tại. Chưa có bản vá mới cho mục này.

session.service.ts: giữ nguyên source hiện tại. Chưa có bản vá mới cho mục này.