Skip to content

feat(studio): add artifact detail workspace#6

Merged
LRriver merged 1 commit into
mainfrom
studio-artifact-detail-view
Jul 9, 2026
Merged

feat(studio): add artifact detail workspace#6
LRriver merged 1 commit into
mainfrom
studio-artifact-detail-view

Conversation

@LRriver

@LRriver LRriver commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replace inline Studio artifact expansion with a dedicated detail view and back navigation.
  • Render Mind Map artifacts as connected interactive graphs with clearer layout, accessible expandable nodes, and non-clickable leaves.
  • Add dark-mode coverage, responsive detail layout fixes, persisted slide deck handoff, and regression tests for artifact viewers.

Validation

  • npm test -- --reporter=dot
  • npm run build
  • npm run lint (0 errors, existing warnings)
  • npm run test:e2e -- --project=chromium
  • python3 -m compileall -q backend
  • python -m pytest -q
  • live local smoke: backend + frontend, upload doc/L9.md, RAG chat, open Mind Map detail

Copilot AI review requested due to automatic review settings July 9, 2026 09:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the interactive Studio artifacts interface, replacing the accordion-style cards with a dedicated list and a full-width detail pane, and introduces a custom-rendered, collapsible MindMapViewer with SVG links. Feedback on these changes focuses on resolving state leakage in ArtifactViewer by adding a unique key, correcting the source count fallback logic, and separating the detail mode cleanup effect to prevent layout flickering. Additionally, suggestions were made to centralize local translations, adjust the mind map node width calculation to support Chinese characters, add a CSS transition for smoother hover animations, and replace a non-standard font weight with a standard one.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

</span>
</div>
{selectedContent.audioUrl && <audio controls src={selectedContent.audioUrl} className="w-full mb-3" />}
<ArtifactViewer content={selectedContent} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Without a unique key (such as selectedContent.id), React will reuse the ArtifactViewer component instance when switching between different artifacts of the same type. This causes internal state (such as openIndex in FAQViewer, cardIndex and answers in FlashcardsViewer, and expandedIds in MindMapViewer) to leak from one artifact to another instead of resetting. Adding key={selectedContent.id} ensures that the viewer resets its state correctly on artifact switch.

Suggested change
<ArtifactViewer content={selectedContent} />
<ArtifactViewer key={selectedContent.id} content={selectedContent} />

].filter(Boolean).join('\n');
};

const contentSourceCount = (content: GeneratedContent) => content.sourceIds === undefined ? sourceIds.length : content.sourceIds.length;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If content.sourceIds is undefined, falling back to sourceIds.length (the currently selected sources in the workspace) will cause the displayed source count of older or restored artifacts to dynamically change whenever the user selects or deselects sources in the sidebar. It is safer and more accurate to fall back to 0 (or handle the undefined case statically) to ensure the source count remains consistent with the artifact's actual generation state.

Suggested change
const contentSourceCount = (content: GeneratedContent) => content.sourceIds === undefined ? sourceIds.length : content.sourceIds.length;
const contentSourceCount = (content: GeneratedContent) => content.sourceIds?.length ?? 0;

Comment on lines +65 to +68
useEffect(() => {
onDetailModeChange?.(Boolean(selectedContent));
return () => onDetailModeChange?.(false);
}, [onDetailModeChange, selectedContent]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When selectedContent changes from one active content to another, the cleanup function () => onDetailModeChange?.(false) runs first, followed immediately by onDetailModeChange?.(true). This rapid state transition (true -> false -> true) can cause unnecessary re-renders or layout flickering in the parent component. Separating the unmount cleanup into its own useEffect avoids this issue.

    useEffect(() => {
        onDetailModeChange?.(Boolean(selectedContent));
    }, [onDetailModeChange, selectedContent]);

    useEffect(() => {
        return () => onDetailModeChange?.(false);
    }, [onDetailModeChange]);

Comment on lines +28 to +41
const TYPE_LABELS: Record<string, { zh: string; en: string }> = {
podcast_script: { zh: '播客', en: 'Podcast' },
mind_map: { zh: '思维导图', en: 'Mind Map' },
faq: { zh: 'FAQ', en: 'FAQ' },
flashcards: { zh: '卡片', en: 'Flashcards' },
quiz: { zh: '测验', en: 'Quiz' },
report: { zh: '报告', en: 'Report' },
study_guide: { zh: '学习指南', en: 'Study Guide' },
data_table: { zh: '数据表', en: 'Data Table' },
infographic: { zh: '信息图', en: 'Infographic' },
video_overview: { zh: '视频概览', en: 'Video Overview' },
ppt_outline: { zh: 'PPT', en: 'PPT' },
slide_deck: { zh: 'PPT', en: 'Slide Deck' }
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The local TYPE_LABELS constant duplicates and slightly modifies translations that are already defined in the centralized translations object in App.tsx (under tools). To maintain a single source of truth for internationalization and avoid duplication, these labels should be centralized in translations in App.tsx and accessed via the t object.

Comment on lines +40 to +44
const mindNodeWidth = (label: string, depth: number) => {
const base = depth === 0 ? 165 : depth === 1 ? 160 : 130;
const max = depth === 0 ? 220 : 200;
return Math.max(base, Math.min(max, label.length * 7 + 46));
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The mindNodeWidth function estimates the node width using label.length * 7. However, Chinese characters (which are common in this project) are significantly wider than English characters (typically around 12-14px vs 7px). This causes Chinese labels to easily overflow or wrap awkwardly within the node. Estimating the width by counting double-byte characters with a larger multiplier (e.g., 12px) would prevent visual truncation.

Suggested change
const mindNodeWidth = (label: string, depth: number) => {
const base = depth === 0 ? 165 : depth === 1 ? 160 : 130;
const max = depth === 0 ? 220 : 200;
return Math.max(base, Math.min(max, label.length * 7 + 46));
};
const mindNodeWidth = (label: string, depth: number) => {
const base = depth === 0 ? 165 : depth === 1 ? 160 : 130;
const max = depth === 0 ? 220 : 200;
const estimatedWidth = [...label].reduce((sum, char) => sum + (char.charCodeAt(0) > 255 ? 12 : 7), 46);
return Math.max(base, Math.min(max, estimatedWidth));
};

Comment thread frontend/src/index.css
Comment on lines +205 to 218
.artifact-list-item {
width: 100%;
display: flex;
min-height: 74px;
display: grid;
grid-template-columns: 42px minmax(0, 1fr) 22px;
align-items: center;
justify-content: space-between;
gap: 10px;
gap: 12px;
padding: 12px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.86);
border: 1px solid rgba(120, 53, 15, 0.08);
box-shadow: 0 8px 20px rgba(120, 53, 15, 0.05);
text-align: left;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The .artifact-list-item:hover class applies a transform: translateY(-1px) and a border-color change, but .artifact-list-item does not have a transition property defined. This makes the hover effect abrupt and jerky. Adding a smooth transition (e.g., transition: all 0.2s ease) will improve the user experience.

Suggested change
.artifact-list-item {
width: 100%;
display: flex;
min-height: 74px;
display: grid;
grid-template-columns: 42px minmax(0, 1fr) 22px;
align-items: center;
justify-content: space-between;
gap: 10px;
gap: 12px;
padding: 12px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.86);
border: 1px solid rgba(120, 53, 15, 0.08);
box-shadow: 0 8px 20px rgba(120, 53, 15, 0.05);
text-align: left;
}
.artifact-list-item {
width: 100%;
min-height: 74px;
display: grid;
grid-template-columns: 42px minmax(0, 1fr) 22px;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.86);
border: 1px solid rgba(120, 53, 15, 0.08);
box-shadow: 0 8px 20px rgba(120, 53, 15, 0.05);
text-align: left;
transition: all 0.2s ease;
}

Comment thread frontend/src/index.css
Comment on lines +1858 to 1876
.mind-graph-node {
position: absolute;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-height: 32px;
border-radius: 999px;
padding: 5px 10px;
margin: 4px 0;
background: white;
border: 1px solid rgba(120, 53, 15, 0.1);
color: #334155;
font-size: 0.82rem;
font-weight: 800;
min-width: 0;
border: 1px solid #bfdbfe;
border-radius: 10px;
padding: 9px 11px;
background: #dbeafe;
color: #111827;
font-size: 0.78rem;
font-weight: 850;
line-height: 1.2;
text-align: left;
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The font weight 850 is non-standard and is not imported in the Google Fonts @import at the top of the file (which only imports up to 800 for Outfit and 700 for Inter). This can lead to inconsistent font rendering across different browsers. It is recommended to use a standard weight like 800.

Suggested change
.mind-graph-node {
position: absolute;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-height: 32px;
border-radius: 999px;
padding: 5px 10px;
margin: 4px 0;
background: white;
border: 1px solid rgba(120, 53, 15, 0.1);
color: #334155;
font-size: 0.82rem;
font-weight: 800;
min-width: 0;
border: 1px solid #bfdbfe;
border-radius: 10px;
padding: 9px 11px;
background: #dbeafe;
color: #111827;
font-size: 0.78rem;
font-weight: 850;
line-height: 1.2;
text-align: left;
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
}
.mind-graph-node {
position: absolute;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-width: 0;
border: 1px solid #bfdbfe;
border-radius: 10px;
padding: 9px 11px;
background: #dbeafe;
color: #111827;
font-size: 0.78rem;
font-weight: 800;
line-height: 1.2;
text-align: left;
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
}

@LRriver LRriver merged commit ccea9ba into main Jul 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants