feat: update replay preview logic, refactor narrative editor state management, and improve overlay UI styling
Build and Release / release (push) Successful in 40s

This commit is contained in:
taDuc
2026-06-02 00:07:07 +07:00
parent d18c29f391
commit 288cde5dcf
5 changed files with 99 additions and 140 deletions
+12 -4
View File
@@ -1108,6 +1108,14 @@ function EditorPageContent() {
timelineFilterEnabled,
]);
const handleEnterPreviewClick = useCallback(() => {
if (mode === "replay") {
openReplayPreview("start");
} else {
openViewerPreview();
}
}, [mode, openReplayPreview, openViewerPreview]);
const viewerPreviewSelectedReplay = useMemo(() => {
if (!isViewerPreviewMode || !selectedFeatureIds.length) return null;
const selectedGeometryId = String(selectedFeatureIds[0] ?? "").trim();
@@ -1212,9 +1220,9 @@ function EditorPageContent() {
}
if (m === "replay" && featureId) {
// QUY TẮC: Geo chọn đầu tiên là geo main.
const finalSelectedIds = Array.from(new Set([...selectedFeatureIds, featureId]));
const triggerId = selectedFeatureIds.length > 0 ? selectedFeatureIds[0] : featureId;
// Sử dụng chính geo được click chuột phải làm main replay geometry
const triggerId = featureId;
const finalSelectedIds = Array.from(new Set([featureId, ...selectedFeatureIds]));
setReplayFeatureId(triggerId);
setReplaySelection({ stageId: null, stepIndex: null });
@@ -2682,7 +2690,7 @@ function EditorPageContent() {
localFeatureIds={localFeatureIds}
showViewportControls={!isReplayPreviewMode || replayPreview.zoomPanelVisible}
isPreviewMode={isAnyPreviewMode}
onEnterPreview={openViewerPreview}
onEnterPreview={handleEnterPreviewClick}
onExitPreview={isReplayPreviewMode ? exitReplayPreview : exitViewerPreview}
onPlayPreviewReplay={viewerPreviewSelectedReplay ? handleMapPlayPreviewReplay : undefined}
viewMode={viewMode}
@@ -520,6 +520,7 @@ export default function ReplayEffectsSidebar({
{selectedStage && selectedStep && selectedStepIndex != null ? (
<>
<ActionGroupEditor
resetKey={`narrative-${selectedStage.id}-${selectedStepIndex}`}
title="Narrative"
groupLabel={`Replay: cập nhật narrative step ${selectedStepIndex + 1} của stage #${selectedStage.id}`}
actions={selectedStep.use_narrow_function}
@@ -1144,6 +1145,7 @@ function ActionGroupEditor<T extends string>({
emptyOptionLabel,
onUpdateActions,
onLinkClick,
resetKey,
}: {
title: string;
groupLabel: string;
@@ -1155,6 +1157,7 @@ function ActionGroupEditor<T extends string>({
emptyOptionLabel?: string;
onUpdateActions: (nextActions: ReplayAction<T>[], label: string) => void;
onLinkClick?: (quill: any) => void;
resetKey?: string;
}) {
const functionNames = useMemo(() => Object.keys(definitions) as T[], [definitions]);
const [composerFunctionName, setComposerFunctionName] = useState<T | "">(
@@ -1167,12 +1170,15 @@ function ActionGroupEditor<T extends string>({
)
);
const lastResetKeyRef = useRef<string | undefined>(undefined);
const lastLoadedActionsRef = useRef<any>(null);
useEffect(() => {
if (JSON.stringify(actions) === JSON.stringify(lastLoadedActionsRef.current)) {
const resetKeyChanged = resetKey !== lastResetKeyRef.current;
if (!resetKeyChanged && JSON.stringify(actions) === JSON.stringify(lastLoadedActionsRef.current)) {
return;
}
lastResetKeyRef.current = resetKey;
lastLoadedActionsRef.current = actions;
if (actions.length > 0) {
@@ -1187,7 +1193,7 @@ function ActionGroupEditor<T extends string>({
setComposerFunctionName(defaultFun);
setComposerDraftValues(buildActionComposerDraft(definitions, defaultFun));
}
}, [actions, definitions, createOnSelect, functionNames]);
}, [actions, definitions, createOnSelect, functionNames, resetKey]);
const composerDefinition = composerFunctionName
? definitions[composerFunctionName]
@@ -1343,6 +1349,29 @@ function FieldInput({
onChange: (nextValue: ActionValue) => void;
onLinkClick?: (quill: any) => void;
}) {
const onLinkClickRef = useRef(onLinkClick);
useEffect(() => {
onLinkClickRef.current = onLinkClick;
}, [onLinkClick]);
const quillModules = useMemo(() => {
return {
toolbar: {
container: [
["bold", "italic", "underline", "strike"],
[{ list: "ordered" }, { list: "bullet" }],
["link"],
["clean"],
],
handlers: {
link: function (this: { quill?: any }) {
onLinkClickRef.current?.(this?.quill);
},
},
},
};
}, []);
const baseLabel = (
<div style={{ fontSize: 12, color: "#cbd5e1", fontWeight: 700 }}>
{field.label}
@@ -1358,21 +1387,7 @@ function FieldInput({
theme="snow"
value={asString(value)}
onChange={(content: string) => onChange(content)}
modules={{
toolbar: {
container: [
["bold", "italic", "underline", "strike"],
[{ list: "ordered" }, { list: "bullet" }],
["link"],
["clean"],
],
handlers: {
link: function (this: { quill?: any }) {
onLinkClick?.(this?.quill);
},
},
},
}}
modules={quillModules}
/>
</div>
</label>
@@ -93,53 +93,69 @@ export default function ReplayPreviewOverlay({
<div
style={{
position: "absolute",
left: 18,
left: 88,
right: rightOffset,
bottom: 96,
borderRadius: 20,
overflow: "hidden",
border: "1px solid rgba(255, 255, 255, 0.1)",
background: "rgba(11, 18, 32, 0.85)",
backdropFilter: "blur(12px)",
boxShadow: "0 16px 44px rgba(2, 6, 23, 0.42)",
pointerEvents: "auto",
pointerEvents: "none",
display: "flex",
flexDirection: "column",
maxHeight: "calc(100vh - 180px)",
justifyContent: "flex-start",
}}
>
{dialog.image_url?.trim() ? (
<img
src={dialog.image_url}
alt="Historical"
style={{
width: "100%",
display: "block",
maxHeight: 140,
objectFit: "cover",
background: "#020617",
}}
/>
) : null}
{dialog.text?.trim() ? (
<div
className="uhm-replay-dialog-content"
style={{
padding: "16px",
color: "#f8fafc",
fontSize: "14px",
lineHeight: "1.6",
overflowY: "auto",
maxHeight: dialog.image_url?.trim() ? "180px" : "140px",
minHeight: 0,
background: "transparent",
}}
dangerouslySetInnerHTML={{ __html: dialog.text }}
/>
) : null}
<div
style={{
width: "min(640px, 100%)",
borderRadius: 20,
overflow: "hidden",
border: "1px solid rgba(255, 255, 255, 0.1)",
background: "rgba(11, 18, 32, 0.85)",
backdropFilter: "blur(12px)",
boxShadow: "0 16px 44px rgba(2, 6, 23, 0.42)",
pointerEvents: "auto",
display: "flex",
flexDirection: "column",
maxHeight: "calc(100vh - 180px)",
}}
>
{dialog.image_url?.trim() ? (
<img
src={dialog.image_url}
alt="Historical"
style={{
width: "100%",
display: "block",
maxHeight: 140,
objectFit: "cover",
background: "#020617",
}}
/>
) : null}
{dialog.text?.trim() ? (
<div
className="uhm-replay-dialog-content"
style={{
padding: "16px",
color: "#f8fafc",
fontSize: "14px",
lineHeight: "1.6",
overflowY: "auto",
maxHeight: dialog.image_url?.trim() ? "180px" : "140px",
minHeight: 0,
background: "transparent",
}}
dangerouslySetInnerHTML={{ __html: dialog.text }}
/>
) : null}
</div>
</div>
) : null}
<style jsx>{`
.uhm-replay-dialog-content::-webkit-scrollbar {
display: none;
}
.uhm-replay-dialog-content {
scrollbar-width: none;
-ms-overflow-style: none;
}
.uhm-replay-dialog-content :global(p) {
margin: 0;
}
-2
View File
@@ -154,7 +154,6 @@ export type UIOptionName =
| "layer_panel"
| "zoom_panel"
| "wiki"
| "toast";
export type MapFunctionName =
| "set_camera_view"
@@ -226,7 +225,6 @@ export type ReplayUiParamTupleDocs = {
layer_panel: [visible: boolean];
zoom_panel: [visible: boolean];
wiki: [wiki_id: string | null];
toast: [message: string];
};
/**