Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit fae4334

Browse filesBrowse files
feat: add Terms of Use dialog with onboarding tour integration (#148)
* feat: add Terms of Use dialog on first launch - Display Terms of Use dialog on first app launch - Replace isFirstLaunch with hasAcceptedTerms flag - Remove auto-start tour behavior (tour now manual only via Help button) - Add translations for all 5 languages (en, ja, ko, zh-CN, zh-TW) - Close editor when user cancels Terms acceptance 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: auto-start onboarding tour after accepting Terms - Start tour automatically when user accepts Terms of Use - Fix onboarding tour button blur with text-shadow: none --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 735033b commit fae4334
Copy full SHA for fae4334

11 files changed

+371-11Lines changed: 371 additions & 11 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎src/extension/commands/open-editor.ts‎

Copy file name to clipboardExpand all lines: src/extension/commands/open-editor.ts
+20-7Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,20 +127,16 @@ export function registerOpenEditorCommand(
127127
// Set webview HTML content
128128
currentPanel.webview.html = getWebviewContent(currentPanel.webview, context.extensionUri);
129129

130-
// Check if this is the first launch and send initial state
131-
const hasLaunchedBefore = context.globalState.get<boolean>('hasLaunchedBefore', false);
132-
if (!hasLaunchedBefore) {
133-
// Mark as launched
134-
context.globalState.update('hasLaunchedBefore', true);
135-
}
130+
// Check if user has accepted terms of use
131+
const hasAcceptedTerms = context.globalState.get<boolean>('hasAcceptedTerms', false);
136132

137133
// Send initial state to webview after a short delay to ensure webview is ready
138134
setTimeout(() => {
139135
if (currentPanel) {
140136
currentPanel.webview.postMessage({
141137
type: 'INITIAL_STATE',
142138
payload: {
143-
isFirstLaunch: !hasLaunchedBefore,
139+
hasAcceptedTerms,
144140
},
145141
});
146142

@@ -241,6 +237,23 @@ export function registerOpenEditorCommand(
241237
console.log('STATE_UPDATE:', message.payload);
242238
break;
243239

240+
case 'ACCEPT_TERMS':
241+
// User accepted terms of use
242+
await context.globalState.update('hasAcceptedTerms', true);
243+
// Update webview with new state
244+
webview.postMessage({
245+
type: 'INITIAL_STATE',
246+
payload: {
247+
hasAcceptedTerms: true,
248+
},
249+
});
250+
break;
251+
252+
case 'CANCEL_TERMS':
253+
// User cancelled terms of use - close the panel
254+
currentPanel?.dispose();
255+
break;
256+
244257
case 'GENERATE_WORKFLOW':
245258
// AI-assisted workflow generation
246259
if (message.payload) {
Collapse file

‎src/shared/types/messages.ts‎

Copy file name to clipboardExpand all lines: src/shared/types/messages.ts
+3-1Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export interface WorkflowListPayload {
5353
}
5454

5555
export interface InitialStatePayload {
56-
isFirstLaunch: boolean;
56+
hasAcceptedTerms: boolean;
5757
}
5858

5959
// ============================================================================
@@ -827,6 +827,8 @@ export type WebviewMessage =
827827
| Message<void, 'LOAD_WORKFLOW_LIST'>
828828
| Message<LoadWorkflowRequestPayload, 'LOAD_WORKFLOW'>
829829
| Message<StateUpdatePayload, 'STATE_UPDATE'>
830+
| Message<void, 'ACCEPT_TERMS'>
831+
| Message<void, 'CANCEL_TERMS'>
830832
| Message<GenerateWorkflowPayload, 'GENERATE_WORKFLOW'>
831833
| Message<CancelGenerationPayload, 'CANCEL_GENERATION'>
832834
| Message<void, 'BROWSE_SKILLS'>
Collapse file

‎src/webview/src/App.tsx‎

Copy file name to clipboardExpand all lines: src/webview/src/App.tsx
+29-3Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { SimpleOverlay } from './components/common/SimpleOverlay';
1818
import { ConfirmDialog } from './components/dialogs/ConfirmDialog';
1919
import { RefinementChatPanel } from './components/dialogs/RefinementChatPanel';
2020
import { SlackShareDialog } from './components/dialogs/SlackShareDialog';
21+
import { TermsOfUseDialog } from './components/dialogs/TermsOfUseDialog';
2122
import { ErrorNotification } from './components/ErrorNotification';
2223
import { NodePalette } from './components/NodePalette';
2324
import { PropertyPanel } from './components/PropertyPanel';
@@ -48,6 +49,7 @@ const App: React.FC = () => {
4849
const [tourKey, setTourKey] = useState(0); // Used to force Tour component remount
4950
const [isSlackShareDialogOpen, setIsSlackShareDialogOpen] = useState(false);
5051
const [isLoadingImportedWorkflow, setIsLoadingImportedWorkflow] = useState(false);
52+
const [showTermsDialog, setShowTermsDialog] = useState(false);
5153

5254
const handleError = (errorData: ErrorPayload) => {
5355
setError(errorData);
@@ -70,16 +72,33 @@ const App: React.FC = () => {
7072
setIsSlackShareDialogOpen(true);
7173
};
7274

75+
const handleAcceptTerms = () => {
76+
// Send accept message to Extension
77+
vscode.postMessage({
78+
type: 'ACCEPT_TERMS',
79+
});
80+
setShowTermsDialog(false);
81+
// Start onboarding tour after accepting terms
82+
handleStartTour();
83+
};
84+
85+
const handleCancelTerms = () => {
86+
// Send cancel message to Extension to close the panel
87+
vscode.postMessage({
88+
type: 'CANCEL_TERMS',
89+
});
90+
};
91+
7392
// Listen for messages from Extension
7493
useEffect(() => {
7594
const messageHandler = (event: MessageEvent) => {
7695
const message = event.data;
7796

7897
if (message.type === 'INITIAL_STATE') {
7998
const payload = message.payload as InitialStatePayload;
80-
if (payload.isFirstLaunch) {
81-
// Start tour automatically on first launch
82-
setRunTour(true);
99+
if (!payload.hasAcceptedTerms) {
100+
// Show terms dialog if not accepted
101+
setShowTermsDialog(true);
83102
}
84103
} else if (message.type === 'IMPORT_WORKFLOW_FROM_SLACK') {
85104
// Handle import workflow request from Extension Host
@@ -181,6 +200,13 @@ const App: React.FC = () => {
181200
{/* Error Notification Overlay */}
182201
<ErrorNotification error={error} onDismiss={handleDismissError} />
183202

203+
{/* Terms of Use Dialog */}
204+
<TermsOfUseDialog
205+
isOpen={showTermsDialog}
206+
onAccept={handleAcceptTerms}
207+
onCancel={handleCancelTerms}
208+
/>
209+
184210
{/* Interactive Tour */}
185211
<Tour key={tourKey} run={runTour} onFinish={handleTourFinish} />
186212

Collapse file
+227Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/**
2+
* TermsOfUseDialog Component
3+
*
4+
* Terms of Use dialog for first-time users
5+
*/
6+
7+
import type React from 'react';
8+
import { useEffect, useId, useRef, useState } from 'react';
9+
import { useTranslation } from '../../i18n/i18n-context';
10+
11+
interface TermsOfUseDialogProps {
12+
isOpen: boolean;
13+
onAccept: () => void;
14+
onCancel: () => void;
15+
}
16+
17+
/**
18+
* Terms of Use Dialog Component
19+
*/
20+
export const TermsOfUseDialog: React.FC<TermsOfUseDialogProps> = ({
21+
isOpen,
22+
onAccept,
23+
onCancel,
24+
}) => {
25+
const { t } = useTranslation();
26+
const dialogRef = useRef<HTMLDivElement>(null);
27+
const titleId = useId();
28+
const [agreed, setAgreed] = useState(false);
29+
30+
// ダイアログが開いたときに自動的にフォーカス
31+
useEffect(() => {
32+
if (isOpen && dialogRef.current) {
33+
dialogRef.current.focus();
34+
}
35+
}, [isOpen]);
36+
37+
// ダイアログが閉じたときに状態をリセット
38+
useEffect(() => {
39+
if (!isOpen) {
40+
setAgreed(false);
41+
}
42+
}, [isOpen]);
43+
44+
if (!isOpen) {
45+
return null;
46+
}
47+
48+
return (
49+
<div
50+
style={{
51+
position: 'fixed',
52+
top: 0,
53+
left: 0,
54+
right: 0,
55+
bottom: 0,
56+
backgroundColor: 'rgba(0, 0, 0, 0.7)',
57+
display: 'flex',
58+
alignItems: 'center',
59+
justifyContent: 'center',
60+
zIndex: 10000,
61+
}}
62+
onKeyDown={(e) => {
63+
if (e.key === 'Escape') {
64+
onCancel();
65+
}
66+
}}
67+
>
68+
<div
69+
ref={dialogRef}
70+
tabIndex={-1}
71+
style={{
72+
backgroundColor: 'var(--vscode-editor-background)',
73+
border: '1px solid var(--vscode-panel-border)',
74+
borderRadius: '4px',
75+
padding: '32px',
76+
minWidth: '500px',
77+
maxWidth: '600px',
78+
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.3)',
79+
outline: 'none',
80+
}}
81+
onClick={(e) => e.stopPropagation()}
82+
onKeyDown={(e) => e.stopPropagation()}
83+
>
84+
{/* Title */}
85+
<div
86+
id={titleId}
87+
style={{
88+
fontSize: '18px',
89+
fontWeight: 600,
90+
color: 'var(--vscode-foreground)',
91+
marginBottom: '20px',
92+
}}
93+
>
94+
{t('terms.title')}
95+
</div>
96+
97+
{/* Content */}
98+
<div
99+
style={{
100+
fontSize: '13px',
101+
color: 'var(--vscode-descriptionForeground)',
102+
marginBottom: '24px',
103+
lineHeight: '1.6',
104+
}}
105+
>
106+
{/* Introduction */}
107+
<p style={{ marginBottom: '16px' }}>{t('terms.introduction')}</p>
108+
109+
{/* Prohibited Uses */}
110+
<p style={{ marginBottom: '8px', fontWeight: 500 }}>{t('terms.prohibitedUse')}</p>
111+
<ul style={{ marginLeft: '20px', marginBottom: '16px' }}>
112+
<li style={{ marginBottom: '4px' }}>{t('terms.cyberAttack')}</li>
113+
<li style={{ marginBottom: '4px' }}>{t('terms.malware')}</li>
114+
<li style={{ marginBottom: '4px' }}>{t('terms.personalDataTheft')}</li>
115+
<li style={{ marginBottom: '4px' }}>{t('terms.otherIllegalActs')}</li>
116+
</ul>
117+
118+
{/* Liability */}
119+
<p style={{ marginBottom: '16px', fontWeight: 500 }}>{t('terms.liability')}</p>
120+
</div>
121+
122+
{/* Checkbox */}
123+
<div
124+
style={{
125+
display: 'flex',
126+
alignItems: 'center',
127+
marginBottom: '24px',
128+
cursor: 'pointer',
129+
userSelect: 'none',
130+
}}
131+
onClick={() => setAgreed(!agreed)}
132+
onKeyDown={(e) => {
133+
if (e.key === 'Enter' || e.key === ' ') {
134+
e.preventDefault();
135+
setAgreed(!agreed);
136+
}
137+
}}
138+
>
139+
<input
140+
type="checkbox"
141+
checked={agreed}
142+
onChange={(e) => setAgreed(e.target.checked)}
143+
onClick={(e) => e.stopPropagation()}
144+
style={{
145+
marginRight: '8px',
146+
cursor: 'pointer',
147+
}}
148+
/>
149+
<span
150+
style={{
151+
fontSize: '13px',
152+
color: 'var(--vscode-foreground)',
153+
}}
154+
>
155+
{t('terms.agree')}
156+
</span>
157+
</div>
158+
159+
{/* Buttons */}
160+
<div
161+
style={{
162+
display: 'flex',
163+
gap: '8px',
164+
justifyContent: 'flex-end',
165+
}}
166+
>
167+
<button
168+
type="button"
169+
onClick={onCancel}
170+
style={{
171+
padding: '8px 20px',
172+
backgroundColor: 'var(--vscode-button-secondaryBackground)',
173+
color: 'var(--vscode-button-secondaryForeground)',
174+
border: 'none',
175+
borderRadius: '2px',
176+
cursor: 'pointer',
177+
fontSize: '13px',
178+
}}
179+
onMouseEnter={(e) => {
180+
e.currentTarget.style.backgroundColor =
181+
'var(--vscode-button-secondaryHoverBackground)';
182+
}}
183+
onMouseLeave={(e) => {
184+
e.currentTarget.style.backgroundColor = 'var(--vscode-button-secondaryBackground)';
185+
}}
186+
>
187+
{t('terms.cancelButton')}
188+
</button>
189+
<button
190+
type="button"
191+
onClick={onAccept}
192+
disabled={!agreed}
193+
style={{
194+
padding: '8px 20px',
195+
backgroundColor: agreed
196+
? 'var(--vscode-button-background)'
197+
: 'var(--vscode-button-secondaryBackground)',
198+
color: agreed
199+
? 'var(--vscode-button-foreground)'
200+
: 'var(--vscode-button-secondaryForeground)',
201+
border: 'none',
202+
borderRadius: '2px',
203+
cursor: agreed ? 'pointer' : 'not-allowed',
204+
fontSize: '13px',
205+
fontWeight: 500,
206+
opacity: agreed ? 1 : 0.5,
207+
}}
208+
onMouseEnter={(e) => {
209+
if (agreed) {
210+
e.currentTarget.style.backgroundColor = 'var(--vscode-button-hoverBackground)';
211+
}
212+
}}
213+
onMouseLeave={(e) => {
214+
if (agreed) {
215+
e.currentTarget.style.backgroundColor = 'var(--vscode-button-background)';
216+
}
217+
}}
218+
>
219+
{t('terms.agreeButton')}
220+
</button>
221+
</div>
222+
</div>
223+
</div>
224+
);
225+
};
226+
227+
export default TermsOfUseDialog;
Collapse file

‎src/webview/src/i18n/translation-keys.ts‎

Copy file name to clipboardExpand all lines: src/webview/src/i18n/translation-keys.ts
+15Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,21 @@ export interface WebviewTranslationKeys {
212212
'tour.button.next': string;
213213
'tour.button.skip': string;
214214

215+
// Terms of Use
216+
'terms.title': string;
217+
'terms.introduction': string;
218+
'terms.prohibitedUse': string;
219+
'terms.cyberAttack': string;
220+
'terms.malware': string;
221+
'terms.personalDataTheft': string;
222+
'terms.otherIllegalActs': string;
223+
'terms.liability': string;
224+
'terms.agree': string;
225+
'terms.agreeButton': string;
226+
'terms.cancelButton': string;
227+
'terms.warning.aiGeneration': string;
228+
'terms.warning.workflow': string;
229+
215230
// Delete Confirmation Dialog
216231
'dialog.deleteNode.title': string;
217232
'dialog.deleteNode.message': string;

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.