@ihyeon/flow-qa 0.0.0-stage → 0.1.0-alpha.2

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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.ko.md +73 -0
  3. package/README.md +72 -2
  4. package/THIRD_PARTY_NOTICES.md +9 -0
  5. package/dist/bin/flow-qa.mjs +3 -0
  6. package/dist/examples/demo/server.js +31 -0
  7. package/dist/src/cli.js +285 -0
  8. package/dist/src/compiler.js +62 -0
  9. package/dist/src/engine.js +246 -0
  10. package/dist/src/i18n.js +98 -0
  11. package/dist/src/planner.js +112 -0
  12. package/dist/src/report.js +315 -0
  13. package/dist/src/schema.js +71 -0
  14. package/dist/src/ui/entry.js +109 -0
  15. package/dist/src/ui/evidence.js +85 -0
  16. package/dist/src/ui/public/app.js +360 -0
  17. package/dist/src/ui/public/display.js +56 -0
  18. package/dist/src/ui/public/fonts/dm-sans.woff2 +0 -0
  19. package/dist/src/ui/public/fonts/noto-kr-subset.woff2 +0 -0
  20. package/dist/src/ui/public/i18n.js +290 -0
  21. package/dist/src/ui/public/index.html +117 -0
  22. package/dist/src/ui/public/style.css +200 -0
  23. package/dist/src/ui/server.js +785 -0
  24. package/dist/src/workspace.js +88 -0
  25. package/docs/architecture.md +38 -0
  26. package/docs/releases.md +54 -0
  27. package/docs/ui.md +71 -0
  28. package/licenses/DM-Sans-LICENSE.txt +93 -0
  29. package/licenses/Noto-Sans-KR-LICENSE.txt +93 -0
  30. package/node_modules/@hyeonse/design-system/.github/workflows/ci.yml +21 -0
  31. package/node_modules/@hyeonse/design-system/.npmignore +5 -0
  32. package/node_modules/@hyeonse/design-system/AGENTS.md +10 -0
  33. package/node_modules/@hyeonse/design-system/LICENSE +21 -0
  34. package/node_modules/@hyeonse/design-system/README.md +67 -0
  35. package/node_modules/@hyeonse/design-system/THIRD_PARTY_NOTICES.md +5 -0
  36. package/node_modules/@hyeonse/design-system/dist/components.css +105 -0
  37. package/node_modules/@hyeonse/design-system/dist/preferences.d.ts +21 -0
  38. package/node_modules/@hyeonse/design-system/dist/preferences.js +69 -0
  39. package/node_modules/@hyeonse/design-system/dist/react.d.ts +21 -0
  40. package/node_modules/@hyeonse/design-system/dist/react.js +100 -0
  41. package/node_modules/@hyeonse/design-system/dist/tokens.css +75 -0
  42. package/node_modules/@hyeonse/design-system/docs/api.md +33 -0
  43. package/node_modules/@hyeonse/design-system/docs/assets/fonts/dm-sans.woff2 +0 -0
  44. package/node_modules/@hyeonse/design-system/docs/assets/fonts/noto-kr-subset.woff2 +0 -0
  45. package/node_modules/@hyeonse/design-system/docs/audit.md +29 -0
  46. package/node_modules/@hyeonse/design-system/docs/foundations.md +30 -0
  47. package/node_modules/@hyeonse/design-system/docs/verification.md +13 -0
  48. package/node_modules/@hyeonse/design-system/index.html +51 -0
  49. package/node_modules/@hyeonse/design-system/licenses/DM-Sans-LICENSE.txt +93 -0
  50. package/node_modules/@hyeonse/design-system/licenses/Noto-Sans-KR-LICENSE.txt +93 -0
  51. package/node_modules/@hyeonse/design-system/licenses/arla6ka-MIT.txt +21 -0
  52. package/node_modules/@hyeonse/design-system/package.json +35 -0
  53. package/node_modules/@hyeonse/design-system/scripts/audit-tokens.mjs +48 -0
  54. package/node_modules/@hyeonse/design-system/scripts/build.mjs +12 -0
  55. package/node_modules/@hyeonse/design-system/showcase.css +75 -0
  56. package/node_modules/@hyeonse/design-system/showcase.mjs +63 -0
  57. package/node_modules/@hyeonse/design-system/skills/hyeonse-design-system/SKILL.md +18 -0
  58. package/node_modules/@hyeonse/design-system/skills/hyeonse-design-system/references/api.md +7 -0
  59. package/node_modules/@hyeonse/design-system/skills/hyeonse-design-system/references/build.md +7 -0
  60. package/node_modules/@hyeonse/design-system/skills/hyeonse-design-system/references/foundations.md +7 -0
  61. package/node_modules/@hyeonse/design-system/skills/hyeonse-design-system/references/mapping.md +7 -0
  62. package/node_modules/@hyeonse/design-system/skills/hyeonse-design-system/references/migration.md +5 -0
  63. package/node_modules/@hyeonse/design-system/skills/hyeonse-design-system/references/review-docs.md +5 -0
  64. package/node_modules/@hyeonse/design-system/src/components.css +105 -0
  65. package/node_modules/@hyeonse/design-system/src/preferences.ts +87 -0
  66. package/node_modules/@hyeonse/design-system/src/react.tsx +161 -0
  67. package/node_modules/@hyeonse/design-system/src/tokens.css +75 -0
  68. package/node_modules/@hyeonse/design-system/tests/audit.test.mjs +30 -0
  69. package/node_modules/@hyeonse/design-system/tests/preferences.test.mjs +86 -0
  70. package/node_modules/@hyeonse/design-system/tsconfig.json +16 -0
  71. package/package.json +73 -4
@@ -0,0 +1,290 @@
1
+ (() => {
2
+ 'use strict';
3
+
4
+ const storageKey = 'flowqa-language';
5
+ // Each key has an English default and a Korean translation. Values are text only.
6
+ const copy = {
7
+ 'brand.subtitle': ['QA workspace', '검증 작업 공간'],
8
+ 'nav.menu': ['Menu', '메뉴'],
9
+ 'nav.label': ['Main menu', '주 메뉴'],
10
+ 'nav.caption': ['WORKSPACE', '작업 공간'],
11
+ 'nav.scenario': ['Scenario', '시나리오'],
12
+ 'nav.library': ['Case library', '케이스 보관함'],
13
+ 'nav.history': ['Run history', '실행 기록'],
14
+ 'nav.settings': ['Connections', '연결 설정'],
15
+ 'top.waiting': ['Ready to run', '실행 대기'],
16
+ 'top.checkingConnection': ['Checking connection', '연결 확인 중'],
17
+ 'top.stop': ['Stop task', '작업 중지'],
18
+ 'top.reconnect': ['Reconnect', '다시 연결'],
19
+ 'top.theme': ['Theme', '테마'],
20
+ 'top.themeAria': ['Change theme', '테마 전환'],
21
+ 'top.language': ['Language', '언어'],
22
+ 'scenario.headingFirst': ['Write a flow,', '흐름을 쓰고,'],
23
+ 'scenario.eyebrow': ['01 / SCENARIO', '01 / 시나리오'],
24
+ 'scenario.headingSecond': ['keep the evidence.', '검증을 남깁니다.'],
25
+ 'scenario.intro': ['Define the expected behavior, review the cases, then run them against a selected target.', '기대 동작을 정의하고 케이스를 검토한 뒤, 선택한 대상에서 실행하세요.'],
26
+ 'scenario.flowTitle': ['Flow to test', '검증할 흐름'],
27
+ 'scenario.flowHelp': ['Describe the action and the result you expect to see.', '어떤 행동에 어떤 결과가 보여야 하는지 적어주세요.'],
28
+ 'scenario.flowLabel': ['Scenario and expected behavior', '시나리오와 기대 동작'],
29
+ 'scenario.flowPlaceholder': ['For example: When a user enters a title and selects Add, the same title appears in the list. An empty title shows an error.', '예: 사용자가 제목을 입력하고 추가를 누르면 목록에 동일한 제목이 표시된다. 빈 제목이면 오류 문구가 표시된다.'],
30
+ 'scenario.flowHint': ['Generate cases when an AI planner is ready. You can also download the prompt and write them yourself without a model connection.', 'AI 플래너가 준비되면 케이스를 생성할 수 있습니다. 연결 없이 프롬프트를 내려받아 직접 작성할 수도 있습니다.'],
31
+ 'scenario.generate': ['Generate cases with AI', 'AI로 케이스 생성'],
32
+ 'scenario.prompt': ['Download generation prompt', '생성 프롬프트 다운로드'],
33
+ 'scenario.reviewTitle': ['Review cases', '케이스 검토'],
34
+ 'scenario.reviewHelp': ['Open each case to inspect its actions and expected results.', '각 케이스를 펼쳐 행동과 기대 결과를 살펴보세요.'],
35
+ 'scenario.noDraft': ['No draft', '초안 없음'],
36
+ 'scenario.summaryEmpty': ['No cases to review yet. Load a local example from the library or generate cases from the scenario.', '아직 검토할 케이스가 없습니다. 보관함의 로컬 예제를 불러오거나 왼쪽 시나리오에서 생성하세요.'],
37
+ 'scenario.sourceNone': ['Source: none', '원본: 없음'],
38
+ 'scenario.fromLibrary': ['Load from library', '보관함에서 불러오기'],
39
+ 'scenario.save': ['Save cases separately', '케이스 별도 저장'],
40
+ 'scenario.saveHint': ['Only saved cases can be run.', '저장된 케이스만 실행할 수 있습니다.'],
41
+ 'scenario.technical': ['Edit JSON · Validate · View script', 'JSON 편집 · 검증 · 스크립트 보기'],
42
+ 'scenario.suiteJson': ['Suite JSON', '스위트 JSON'],
43
+ 'scenario.suitePlaceholder': ['Load a local demo case from the library or enter suite JSON.', '보관함의 로컬 데모 케이스를 불러오거나 스위트 JSON을 입력하세요.'],
44
+ 'scenario.newDraft': ['New JSON draft', '새 JSON 초안'],
45
+ 'scenario.compile': ['Validate JSON · Generate script', 'JSON 검증 · 스크립트 생성'],
46
+ 'scenario.script': ['Run script', '실행 스크립트'],
47
+ 'scenario.downloadScript': ['Download script ↓', '스크립트 다운로드 ↓'],
48
+ 'scenario.scriptPlaceholder': ['The script for validated cases will appear here.', '검증된 케이스의 스크립트가 여기에 표시됩니다.'],
49
+ 'scenario.runTitle': ['Review, then run', '검토 후 실행'],
50
+ 'scenario.runHelp': ['Select a target and confirm you have reviewed the cases.', '대상을 선택하고 케이스 내용을 승인하세요.'],
51
+ 'scenario.target': ['Run target', '실행 대상'],
52
+ 'scenario.selectTarget': ['Select a target', '대상을 선택하세요'],
53
+ 'scenario.targetHint': ['You can add a target in Connections.', '연결 설정에서 대상을 추가할 수 있습니다.'],
54
+ 'scenario.reviewConfirm': ['I have reviewed the requirements, case steps, and expected results.', '요구사항, 케이스 단계와 기대 결과를 직접 검토했습니다.'],
55
+ 'scenario.runInitial': ['Validated cases and a run target are required.', '검증된 케이스와 실행 대상이 필요합니다.'],
56
+ 'scenario.stop': ['Stop run', '실행 중지'],
57
+ 'scenario.run': ['Run QA', 'QA 실행'],
58
+ 'library.heading': ['Case library', '케이스 보관함'],
59
+ 'library.eyebrow': ['02 / LIBRARY', '02 / 보관함'],
60
+ 'library.intro': ['Cases saved on disk. Local demo examples are not AI generated results.', '디스크에 저장된 케이스입니다. 로컬 데모 예제는 AI 생성 결과가 아닙니다.'],
61
+ 'library.title': ['Saved suites', '저장된 스위트'],
62
+ 'library.help': ['Load a suite, then review it on the Scenario page.', '불러온 뒤 시나리오 화면에서 검토하세요.'],
63
+ 'history.heading': ['Run history', '실행 기록'],
64
+ 'history.eyebrow': ['03 / RUNS', '03 / 실행'],
65
+ 'history.intro': ['Only runs that actually started appear here. Once results are saved, you can view the detailed report.', '실제로 시작된 실행만 표시합니다. 결과가 저장되면 상세 보고서를 볼 수 있습니다.'],
66
+ 'history.title': ['Runs', '실행 목록'],
67
+ 'history.help': ['Check the completed report for the final status.', '상태는 완료된 보고서 기준으로 확인하세요.'],
68
+ 'history.report': ['Detailed report', '상세 보고서'],
69
+ 'history.downloadMarkdown': ['Download Markdown', 'Markdown 다운로드'],
70
+ 'history.downloadJson': ['Download JSON', 'JSON 다운로드'],
71
+ 'history.evidenceHint': ['The report summarizes results. Open or download screenshots and recordings from each attempt. Evidence files are not included in report downloads.', '보고서는 결과 요약입니다. 스크린샷·화면 기록은 각 시도에서 열거나 개별 다운로드하세요. 증거 파일은 보고서 다운로드에 포함되지 않습니다.'],
72
+ 'settings.heading': ['Connections', '연결 설정'],
73
+ 'settings.eyebrow': ['04 / CONNECTIONS', '04 / 연결 설정'],
74
+ 'settings.intro': ['Configure run targets and models separately. Key values are read only from server environment variables.', '실행 대상과 모델을 따로 설정합니다. 키 값은 서버 환경 변수에서만 읽습니다.'],
75
+ 'settings.runtime': ['Runtime', '실행 환경'],
76
+ 'settings.runtimeInitial': ['Checking installation status.', '설치 상태를 확인하고 있습니다.'],
77
+ 'settings.runtimeHint': ['This shows local installation and configuration. Verify target access, sign-in, and model responses with an actual run.', '이 표시는 로컬 설치·설정 확인입니다. 대상 접근, 로그인과 모델 응답은 실제 실행으로 확인해야 합니다.'],
78
+ 'settings.display': ['Display', '화면 설정'],
79
+ 'settings.displayKicker': ['DISPLAY', '화면'],
80
+ 'settings.displayHelp': ['Choose a comfortable display.', '읽기 편한 조합을 선택하세요.'],
81
+ 'settings.theme': ['Theme', '테마'],
82
+ 'settings.themeSystem': ['System', '기기 설정'],
83
+ 'settings.themeLight': ['Light', '라이트'],
84
+ 'settings.themeDark': ['Dark', '다크'],
85
+ 'settings.accent': ['Accent', '강조색'],
86
+ 'settings.accentCobalt': ['Cobalt', '코발트'],
87
+ 'settings.accentForest': ['Forest', '포레스트'],
88
+ 'settings.accentCoral': ['Coral', '코랄'],
89
+ 'settings.density': ['Density', '간격'],
90
+ 'settings.densityComfortable': ['Comfortable', '여유롭게'],
91
+ 'settings.densityCompact': ['Compact', '촘촘하게'],
92
+ 'settings.corners': ['Corners', '모서리'],
93
+ 'settings.cornersCrisp': ['Crisp', '각지게'],
94
+ 'settings.cornersSoft': ['Soft', '부드럽게'],
95
+ 'settings.motion': ['Motion', '움직임'],
96
+ 'settings.motionFull': ['Full', '기본'],
97
+ 'settings.motionReduced': ['Reduced', '줄이기'],
98
+ 'settings.savedHere': ['Your display choices are saved in this browser.', '선택한 화면 설정은 이 브라우저에 저장됩니다.'],
99
+ 'settings.reset': ['Reset to defaults', '기본값으로 재설정'],
100
+ 'settings.targets': ['Run targets', '실행 대상'],
101
+ 'settings.targetsHelp': ['Add an allowed HTTPS or local address.', '허용된 HTTPS 주소 또는 로컬 주소를 등록하세요.'],
102
+ 'settings.targetName': ['Target name', '대상 이름'],
103
+ 'settings.targetNamePlaceholder': ['e.g. local-demo', '예: local-demo'],
104
+ 'settings.description': ['Description', '설명'],
105
+ 'settings.descriptionPlaceholder': ['What this target is for', '이 대상의 용도'],
106
+ 'settings.saveTarget': ['Save target', '대상 저장'],
107
+ 'settings.models': ['Model connections', '모델 연결'],
108
+ 'settings.modelsHelp': ['Manage planner and run agent connections separately.', '플래너와 실행 에이전트의 연결을 따로 관리합니다.'],
109
+ 'settings.modelsHint': ['Do not enter or store API key values here. Specify only the name of an environment variable in the server process.', 'API 키 값은 이 화면에 입력하거나 저장하지 않습니다. 서버 프로세스의 환경 변수 이름만 지정하세요.'],
110
+ 'display.themeSystemShort': ['System', '기기 설정'],
111
+ 'display.themeLightShort': ['Light', '밝게'],
112
+ 'display.themeDarkShort': ['Dark', '어둡게'],
113
+ 'display.currentTheme': ['Change theme: currently {theme}', '테마 변경: 현재 {theme}'],
114
+ 'display.themeButton': ['Display · {theme}', '화면 · {theme}'],
115
+ 'display.motionSystemReduced': ['motion reduced by system setting', '기기 설정에 따라 움직임 감소'],
116
+ 'app.breadcrumb': ['Workspace / {name}', '작업 공간 / {name}'],
117
+ 'app.responseUnreadable': ['Could not read the server response ({status}).', '서버 응답을 읽을 수 없습니다 ({status}).'],
118
+ 'app.requestFailed': ['Request failed ({status})', '요청 실패 ({status})'],
119
+ 'app.stopPlan': ['Cancel generation', '생성 취소'],
120
+ 'app.connectionRequired': ['Server connection required. Select Reconnect above.', '서버 연결이 필요합니다. 상단에서 다시 연결해 주세요.'],
121
+ 'app.stopping': ['Processing stop request.', '중지 요청을 처리 중입니다.'],
122
+ 'app.planning': ['AI is generating cases. You can cancel above.', 'AI가 케이스를 생성하고 있습니다. 상단에서 취소할 수 있습니다.'],
123
+ 'app.running': ['Run in progress. Check Run history for results when it finishes.', '실행 중입니다. 완료되면 실행 기록에서 결과를 확인하세요.'],
124
+ 'app.stale': ['Scenario changed. Generate cases again or start a new JSON draft.', '시나리오가 변경됐습니다. 다시 생성하거나 새 JSON 초안을 시작하세요.'],
125
+ 'app.dirty': ['JSON changed. Validate it again.', 'JSON이 변경됐습니다. 다시 검증해 주세요.'],
126
+ 'app.needValidated': ['Validated cases required.', '검증된 케이스가 필요합니다.'],
127
+ 'app.needSaved': ['Save cases or reload them from the library before running.', '실행 전 케이스를 저장하거나 보관함에서 다시 불러오세요.'],
128
+ 'app.needTarget': ['Select a run target.', '실행 대상을 선택하세요.'],
129
+ 'app.needReview': ['Review the contents, then select the checkbox.', '내용 검토 후 확인란을 선택하세요.'],
130
+ 'app.ready': ['Ready to run.', '실행할 준비가 됐습니다.'],
131
+ 'app.caseReadiness': ['{total} total · {ready} runnable · {blocked} blocked before run{hint}', '전체 {total}개 · 실행 가능 {ready}개 · 사전 차단 {blocked}개{hint}'],
132
+ 'app.blockedHint': [' — Check open questions or run agent settings.', ' — 확인 질문이나 실행 모델 설정을 확인하세요.'],
133
+ 'app.regenerate': ['Regenerate required', '재생성 필요'],
134
+ 'app.revalidate': ['Revalidate required', '재검증 필요'],
135
+ 'app.validated': ['Validated', '검증 완료'],
136
+ 'app.sourceGenerated': ['Source: AI generated from this scenario · JSON can be reviewed', '원본: 현재 시나리오에서 AI 생성 · JSON 검토 가능'],
137
+ 'app.sourceLibraryFlow': ['Source: saved cases and linked scenario', '원본: 저장된 케이스와 연결된 시나리오'],
138
+ 'app.sourceLibrary': ['Source: saved cases · no linked scenario', '원본: 저장된 케이스 · 연결된 시나리오 없음'],
139
+ 'app.sourceManualFlow': ['Source: current scenario · manually written JSON', '원본: 현재 시나리오 · 직접 작성한 JSON'],
140
+ 'app.sourceManual': ['Source: manually written JSON · no linked scenario', '원본: 직접 작성한 JSON · 연결된 시나리오 없음'],
141
+ 'app.locatorRole': ['role', '역할'],
142
+ 'app.locatorText': ['text', '텍스트'],
143
+ 'app.locatorLabel': ['label', '레이블'],
144
+ 'app.locatorTestId': ['test ID', '테스트 ID'],
145
+ 'app.locatorName': ['name “{name}”', '이름 “{name}”'],
146
+ 'app.stepOpen': ['Open page: {path}', '페이지 열기: {path}'],
147
+ 'app.stepClick': ['Click: {target}', '클릭: {target}'],
148
+ 'app.stepFill': ['Fill: {target} → “{value}”', '입력: {target} → “{value}”'],
149
+ 'app.stepVisible': ['Expected: {target} visible', '기대 결과: {target} 표시됨'],
150
+ 'app.stepHidden': ['Expected: {target} hidden', '기대 결과: {target} 보이지 않음'],
151
+ 'app.stepTextContains': ['Expected: {target} text contains “{expected}”', '기대 결과: {target} 텍스트가 “{expected}” 포함'],
152
+ 'app.stepTextEquals': ['Expected: {target} text equals “{expected}”', '기대 결과: {target} 텍스트가 “{expected}” 일치'],
153
+ 'app.stepUrl': ['Expected: URL {expected}', '기대 결과: URL {expected}'],
154
+ 'app.stepAct': ['AI action: {instruction}', 'AI 행동: {instruction}'],
155
+ 'app.stepAssertAgent': ['Expected (AI judgment): {instruction}', '기대 결과 (AI 판단): {instruction}'],
156
+ 'app.untitledSuite': ['Untitled suite', '제목 없는 스위트'],
157
+ 'app.cases': ['Cases', '케이스'],
158
+ 'app.requirements': ['Requirements', '요구사항'],
159
+ 'app.questions': ['Open questions', '확인 질문'],
160
+ 'app.caseSteps': ['Case actions and expected results', '케이스별 행동과 기대 결과'],
161
+ 'app.caseBlocked': ['blocked from running', '실행 차단'],
162
+ 'app.linkedRequirements': ['Linked requirements: {ids}', '연결 요구사항: {ids}'],
163
+ 'app.none': ['none', '없음'],
164
+ 'app.preconditions': ['Preconditions', '시작 조건'],
165
+ 'app.blockingQuestions': ['Questions blocking this case', '해결 전 실행이 차단되는 질문'],
166
+ 'app.modelPlanner': ['AI planner', 'AI 플래너'],
167
+ 'app.modelAgent': ['Run agent', '실행 에이전트'],
168
+ 'app.modelPlannerHelp': ['Generates cases from scenarios.', '시나리오에서 케이스를 생성합니다.'],
169
+ 'app.modelAgentHelp': ['Runs cases with natural-language steps.', '자연어 단계가 포함된 케이스 실행에 사용합니다.'],
170
+ 'app.modelUnconnected': ['Not connected.', '연결되지 않았습니다.'],
171
+ 'app.modelExternal': ['An external connection is configured and cannot be edited here.', '외부 연결이 설정되어 있으며 이 화면에서는 수정할 수 없습니다.'],
172
+ 'app.modelGeneric': ['model', '모델'],
173
+ 'app.modelNoKey': ['configured to request without a key', '키 없이 요청하도록 설정됨'],
174
+ 'app.modelKeyReady': ['key environment variable ready', '키 환경 변수 준비됨'],
175
+ 'app.modelKeyMissing': ['key environment variable missing', '키 환경 변수 미설정'],
176
+ 'app.modelUnverified': ['actual call unverified', '실제 호출 미확인'],
177
+ 'app.modelBaseUrl': ['OpenAI-compatible Base URL', 'OpenAI 호환 Base URL'],
178
+ 'app.modelName': ['Model name', '모델 이름'],
179
+ 'app.modelIdentifier': ['Model identifier', '모델 식별자'],
180
+ 'app.modelKeyEnv': ['Key environment variable name (optional)', '키 환경 변수 이름 (선택)'],
181
+ 'app.saveConnection': ['Save connection', '연결 저장'],
182
+ 'app.clearConnection': ['Disconnect', '연결 해제'],
183
+ 'app.libraryEmpty': ['No saved cases. Validate and save JSON from Scenario, or prepare a local demo case.', '저장된 케이스가 없습니다. 시나리오에서 JSON을 검증해 저장하거나 로컬 데모 케이스를 준비하세요.'],
184
+ 'app.libraryMeta': ['{id} · {count} cases · {questions} open questions · file {file}', '{id} · {count}개 케이스 · 확인 질문 {questions}개 · 파일 {file}'],
185
+ 'app.load': ['Load', '불러오기'],
186
+ 'app.historyActiveEmpty': ['First run in progress. Its report will appear here when complete.', '첫 실행을 처리 중입니다. 완료되면 이곳에 보고서가 표시됩니다.'],
187
+ 'app.historyEmpty': ['No runs yet. Validate cases and select a target to start the first run.', '아직 실행 기록이 없습니다. 케이스를 검증하고 대상을 선택해 첫 실행을 시작하세요.'],
188
+ 'app.noTarget': ['no target', '대상 없음'],
189
+ 'app.runMeta': ['{date} · {target} · {passed}/{total} passed · {id}', '{date} · {target} · {passed}/{total} 통과 · {id}'],
190
+ 'app.viewReport': ['View report', '보고서 보기'],
191
+ 'app.disconnected': ['Server disconnected', '서버 연결 끊김'],
192
+ 'app.activeStopping': ['Stopping', '중지 중'],
193
+ 'app.activePlanning': ['Generating cases', '케이스 생성 중'],
194
+ 'app.activeRunning': ['Running', '실행 중'],
195
+ 'app.requestingPlan': ['Requesting generation', '생성 요청 중'],
196
+ 'app.connected': ['Local server connected', '로컬 서버 연결됨'],
197
+ 'app.refreshSettings': ['Refresh settings', '설정 새로고침'],
198
+ 'app.runtimeDisconnected': ['Connect to the server to check the runtime.', '서버 연결 후 실행 환경을 확인할 수 있습니다.'],
199
+ 'app.runtimeInstalled': ['Browser installed · Fixed-action cases can run without a model.', '브라우저 설치 확인됨 · 고정 조작 케이스는 모델 없이 실행할 수 있습니다.'],
200
+ 'app.runtimeMissing': ['Browser installation required. Run {command} in a terminal. Automatic installation on first run can fail depending on network access.', '브라우저 설치가 필요합니다. 터미널에서 {command}을 실행하세요. 첫 실행의 자동 설치는 네트워크 환경에 따라 실패할 수 있습니다.'],
201
+ 'app.plannerUnconfigured': ['AI planner is not connected. Configure a model in Connections or download the prompt.', 'AI 플래너가 연결되지 않았습니다. 연결 설정에서 모델을 지정하거나 프롬프트를 내려받으세요.'],
202
+ 'app.plannerKeyMissing': ['The configured key environment variable is missing on the server. Add it and restart the server.', '설정한 키 환경 변수가 서버에 없습니다. 키를 주입한 뒤 서버를 재시작하세요.'],
203
+ 'app.plannerCliOnly': ['This model can be used from the CLI. Connect an OpenAI-compatible model to generate cases in the UI.', '이 모델은 CLI에서 사용할 수 있습니다. UI에서 생성하려면 OpenAI 호환 모델을 연결하세요.'],
204
+ 'app.plannerReady': ['Ready to generate. The actual model call and costs may occur when you select Generate.', '생성 준비가 됐습니다. 실제 모델 호출과 비용은 생성 버튼을 누를 때 발생할 수 있습니다.'],
205
+ 'app.runEnded': ['Run ended. Check Run history for results.', '실행이 종료됐습니다. 실행 기록에서 결과를 확인하세요.'],
206
+ 'app.suiteLoaded': ['Saved cases loaded. Editing the linked scenario requires generating or loading cases again.', '저장된 케이스를 불러왔습니다. 연결된 시나리오를 수정하면 케이스를 다시 생성하거나 불러와야 합니다.'],
207
+ 'app.enterFlow': ['Enter a scenario with expected behavior.', '기대 동작을 포함한 시나리오를 입력하세요.'],
208
+ 'app.configurePlanner': ['Configure an OpenAI-compatible AI planner and key environment variable in Connections.', '연결 설정에서 OpenAI 호환 AI 플래너와 키 환경 변수를 준비하세요.'],
209
+ 'app.planStarted': ['AI is generating cases. You can cancel above when the task appears.', 'AI가 케이스를 생성하고 있습니다. 작업이 표시되면 상단에서 취소할 수 있습니다.'],
210
+ 'app.planCancelledLate': ['Generation cancelled. A late result was ignored.', '생성을 취소했습니다. 늦게 도착한 결과는 반영하지 않았습니다.'],
211
+ 'app.planComplete': ['Cases generated. Review the actions and expected results, then save.', '케이스가 생성됐습니다. 행동과 기대 결과를 검토하고 저장하세요.'],
212
+ 'app.planCancelledDraft': ['Generation cancelled. Existing draft kept.', '생성을 취소했습니다. 기존 초안을 유지합니다.'],
213
+ 'app.promptDownloaded': ['Prompt downloaded. Paste the generated JSON and validate it.', '프롬프트를 다운로드했습니다. 생성된 JSON을 붙여 넣고 검증하세요.'],
214
+ 'app.compileStale': ['Scenario changed. Generate cases again or reload them from the library.', '시나리오가 변경됐습니다. 다시 생성하거나 보관함의 케이스를 불러오세요.'],
215
+ 'app.invalidJson': ['Check the JSON syntax. Use double quotes around property names and strings, and check commas and brackets.', 'JSON 구문을 확인하세요. 속성 이름과 문자열은 큰따옴표로 감싸고 쉼표·괄호를 확인해 주세요.'],
216
+ 'app.compileComplete': ['JSON validated and script generated. The linked source scenario was kept.', 'JSON 검증과 스크립트 생성이 완료됐습니다. 연결된 원본 시나리오는 유지됩니다.'],
217
+ 'app.savedSuite': ['Cases saved to a separate file: {file}. Review them again before running.', '케이스를 별도 파일로 저장했습니다: {file}. 실행 전 내용을 다시 검토하세요.'],
218
+ 'app.runNotReady': ['Save or reload cases, select a target, and review the contents before running.', '케이스 저장 또는 다시 불러오기, 대상 선택, 직접 검토가 필요합니다.'],
219
+ 'app.runStarted': ['Run started. Task ID: {id}', '실행을 시작했습니다. 작업 ID: {id}'],
220
+ 'app.stopRequested': ['Stop request sent. Checking the final state.', '중지 요청을 보냈습니다. 실제 종료 상태를 확인 중입니다.'],
221
+ 'app.reportStatus': ['Status {status}', '상태 {status}'],
222
+ 'app.reportTarget': ['Target {target}', '대상 {target}'],
223
+ 'app.reportStarted': ['Started {date}', '시작 {date}'],
224
+ 'app.reportFinished': ['Finished {date}', '완료 {date}'],
225
+ 'app.unavailable': ['unavailable', '미제공'],
226
+ 'app.usage': ['Run model tokens {tokens} · Estimated cost {cost} · Case generation cost not included.', '실행 모델 토큰 {tokens} · 추정 비용 {cost} · 케이스 생성 비용은 포함하지 않습니다.'],
227
+ 'app.reason': ['Reason: {reason}', '사유: {reason}'],
228
+ 'app.expected': ['Expected result', '기대 결과'],
229
+ 'app.attempt': ['Attempt {index} · {status} · {duration}ms{error}', '시도 {index} · {status} · {duration}ms{error}'],
230
+ 'app.secondaryError': ['Additional error: {code}: {message}', '추가 오류: {code}: {message}'],
231
+ 'app.viewEvidence': ['View evidence ↗', '증거 보기 ↗'],
232
+ 'app.download': ['Download ↓', '다운로드 ↓'],
233
+ 'app.evidenceUnavailable': ['Unavailable or unsafe to open', '미제공 또는 안전하게 열 수 없는 형식'],
234
+ 'app.modelSaved': ['Model connection saved. Key values are read only from server environment variables.', '모델 연결을 저장했습니다. 키 값은 서버 환경 변수에서만 읽습니다.'],
235
+ 'app.modelCleared': ['Model connection removed.', '모델 연결을 해제했습니다.'],
236
+ 'app.targetSaved': ['Run target saved.', '실행 대상을 저장했습니다.'],
237
+ 'app.newDraftNotice': ['Scenario kept and draft cleared. Paste new JSON and validate it. The saved source file remains.', '시나리오는 유지하고 초안을 비웠습니다. 새 JSON을 붙여 넣고 검증하세요. 저장된 원본 파일은 유지됩니다.'],
238
+ 'status.passed': ['passed', '통과'],
239
+ 'status.failed': ['failed', '실패'],
240
+ 'status.flaky': ['flaky', '불안정'],
241
+ 'status.blocked': ['blocked', '차단'],
242
+ 'status.skipped': ['skipped', '건너뜀'],
243
+ 'status.interrupted': ['interrupted', '중단됨'],
244
+ 'status.running': ['running', '실행 중'],
245
+ 'status.stopping': ['stopping', '중지 중'],
246
+ 'status.stopped': ['stopped', '중지됨'],
247
+ 'status.completed': ['completed', '완료'],
248
+ 'status.cancelled': ['cancelled', '취소됨'],
249
+ 'status.error': ['error', '오류'],
250
+ 'artifact.screenshot': ['screenshot', '스크린샷'],
251
+ 'artifact.video': ['video', '화면 기록'],
252
+ 'artifact.trace': ['trace', '추적 기록'],
253
+ 'artifact.log': ['log', '로그'],
254
+ 'artifact.text': ['text', '텍스트'],
255
+ 'artifact.json': ['JSON', 'JSON']
256
+ };
257
+
258
+ function valid(value) { return value === 'en' || value === 'ko'; }
259
+ let current = 'en';
260
+ try { const stored = localStorage.getItem(storageKey); if (valid(stored)) current = stored; } catch { /* Storage may be disabled. */ }
261
+ const listeners = new Set();
262
+ function t(key, params = {}) {
263
+ const pair = copy[key];
264
+ if (!pair) throw new Error(`Missing translation: ${key}`);
265
+ return pair[current === 'ko' ? 1 : 0].replace(/\{([A-Za-z][A-Za-z0-9]*)\}/g, (_, name) => String(params[name] ?? ''));
266
+ }
267
+ function status(value) { return copy[`status.${value}`] ? t(`status.${value}`) : String(value ?? ''); }
268
+ function artifact(value) { return copy[`artifact.${value}`] ? t(`artifact.${value}`) : String(value ?? ''); }
269
+ function applyStatic() {
270
+ document.documentElement.lang = current;
271
+ document.querySelectorAll('[data-i18n]').forEach((node) => { node.textContent = t(node.dataset.i18n); });
272
+ document.querySelectorAll('[data-i18n-placeholder]').forEach((node) => { node.placeholder = t(node.dataset.i18nPlaceholder); });
273
+ document.querySelectorAll('[data-i18n-aria-label]').forEach((node) => { node.setAttribute('aria-label', t(node.dataset.i18nAriaLabel)); });
274
+ const select = document.getElementById('language-select');
275
+ if (select) select.value = current;
276
+ }
277
+ function change(next, persist) {
278
+ if (!valid(next) || next === current) return;
279
+ current = next;
280
+ if (persist) { try { localStorage.setItem(storageKey, next); } catch { /* Selection still works in this tab. */ } }
281
+ applyStatic();
282
+ for (const listener of listeners) listener(next);
283
+ }
284
+ window.addEventListener('storage', (event) => {
285
+ if (event.key !== storageKey) return;
286
+ change(valid(event.newValue) ? event.newValue : 'en', false);
287
+ });
288
+ document.documentElement.lang = current;
289
+ window.FlowQAI18n = Object.freeze({ locale: () => current, t, status, artifact, applyStatic, setLocale: (next) => change(next, true), onChange: (listener) => { listeners.add(listener); return () => listeners.delete(listener); }, storageKey });
290
+ })();
@@ -0,0 +1,117 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="color-scheme" content="light dark">
7
+ <title>Flow QA</title>
8
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' fill='%23101114'/%3E%3Ctext x='15' y='47' font-size='42' font-family='Georgia' fill='white'%3EF%3C/text%3E%3C/svg%3E">
9
+ <script src="/design-system/init.js"></script>
10
+ <link rel="stylesheet" href="/design-system/tokens.css">
11
+ <link rel="stylesheet" href="/design-system/components.css">
12
+ <link rel="stylesheet" href="/style.css">
13
+ <script src="/i18n.js"></script>
14
+ <script type="module" src="/display.js"></script>
15
+ <script src="/app.js" defer></script>
16
+ </head>
17
+ <body>
18
+ <div class="shell">
19
+ <aside class="sidebar" id="sidebar">
20
+ <div class="brand">
21
+ <span class="brand-mark" aria-hidden="true">F<span>.</span></span>
22
+ <span class="brand-name"><strong>Flow QA</strong><small data-i18n="brand.subtitle">QA workspace</small></span>
23
+ <button class="mobile-toggle" id="nav-toggle" type="button" aria-expanded="false" aria-controls="nav" data-i18n="nav.menu">Menu</button>
24
+ </div>
25
+ <nav id="nav" aria-label="Main menu" data-i18n-aria-label="nav.label">
26
+ <p class="nav-caption" data-i18n="nav.caption">WORKSPACE</p>
27
+ <button type="button" class="nav-item selected" data-view="scenario" aria-current="page"><span class="nav-num">01</span><span data-i18n="nav.scenario">Scenario</span></button>
28
+ <button type="button" class="nav-item" data-view="library"><span class="nav-num">02</span><span data-i18n="nav.library">Case library</span></button>
29
+ <button type="button" class="nav-item" data-view="history"><span class="nav-num">03</span><span data-i18n="nav.history">Run history</span></button>
30
+ <button type="button" class="nav-item" data-view="settings"><span class="nav-num">04</span><span data-i18n="nav.settings">Connections</span></button>
31
+ </nav>
32
+ <div class="side-foot"><span class="signal" id="connection-dot" aria-hidden="true"></span><span id="connection-label" data-i18n="top.checkingConnection">Checking connection</span></div>
33
+ </aside>
34
+ <main class="main">
35
+ <header class="topline">
36
+ <span id="breadcrumb">Workspace / Scenario</span>
37
+ <div class="top-actions"><span class="topline-right" id="active-indicator" role="status" aria-live="polite" data-i18n="top.waiting">Ready to run</span><button type="button" id="global-stop-btn" class="button quiet" hidden data-i18n="top.stop">Stop task</button><button type="button" id="reconnect-btn" class="text-button" data-i18n="top.reconnect">Reconnect</button><label class="language-control"><span data-i18n="top.language">Language</span><select id="language-select" aria-label="Language" data-i18n-aria-label="top.language"><option value="en">English</option><option value="ko">한국어</option></select></label><button type="button" id="theme-toggle" class="hds-themeToggle" aria-label="Change theme">Theme</button></div>
38
+ </header>
39
+ <div class="content">
40
+ <div id="notice" class="notice" role="status" aria-live="polite" hidden></div>
41
+ <section id="view-scenario" class="view">
42
+ <div class="page-heading">
43
+ <p class="eyebrow" data-i18n="scenario.eyebrow">01 / SCENARIO</p>
44
+ <h1><span data-i18n="scenario.headingFirst">Write a flow,</span><br><em data-i18n="scenario.headingSecond">keep the evidence.</em></h1>
45
+ <p data-i18n="scenario.intro">Define the expected behavior, review the cases, then run them against a selected target.</p>
46
+ </div>
47
+ <div class="workflow">
48
+ <section class="work-section flow-section" aria-labelledby="flow-title">
49
+ <div class="section-head"><span class="section-number">01</span><div><h2 id="flow-title" data-i18n="scenario.flowTitle">Flow to test</h2><p data-i18n="scenario.flowHelp">Describe the action and the result you expect to see.</p></div></div>
50
+ <div class="section-body">
51
+ <label for="flow-input" class="field-label" data-i18n="scenario.flowLabel">Scenario and expected behavior</label>
52
+ <textarea id="flow-input" rows="11" placeholder="For example: When a user enters a title and selects Add, the same title appears in the list. An empty title shows an error." data-i18n-placeholder="scenario.flowPlaceholder"></textarea>
53
+ <p class="hint" data-i18n="scenario.flowHint">Generate cases when an AI planner is ready. You can also download the prompt and write them yourself without a model connection.</p>
54
+ <p id="planner-readiness" class="hint" role="status"></p>
55
+ <div class="actions flow-actions"><button id="plan-btn" type="button" class="button primary"><span data-i18n="scenario.generate">Generate cases with AI</span><span aria-hidden="true">↗</span></button><button id="prompt-btn" type="button" class="button quiet" data-i18n="scenario.prompt">Download generation prompt</button></div>
56
+ </div>
57
+ </section>
58
+ <section class="work-section draft-section" aria-labelledby="draft-title">
59
+ <div class="section-head"><span class="section-number">02</span><div><h2 id="draft-title" data-i18n="scenario.reviewTitle">Review cases</h2><p data-i18n="scenario.reviewHelp">Open each case to inspect its actions and expected results.</p></div><span id="draft-status" class="status-pill" data-i18n="scenario.noDraft">No draft</span></div>
60
+ <div class="section-body">
61
+ <div class="draft-topline"><span id="draft-source" data-i18n="scenario.sourceNone">Source: none</span><button type="button" class="text-button" id="library-shortcut"><span data-i18n="scenario.fromLibrary">Load from library</span><span aria-hidden="true">↗</span></button></div>
62
+ <div id="draft-summary" class="summary" aria-live="polite"><p class="summary-empty" data-i18n="scenario.summaryEmpty">No cases to review yet. Load a local example from the library or generate cases from the scenario.</p></div>
63
+ <div class="draft-actions"><button id="save-btn" type="button" class="button" data-i18n="scenario.save">Save cases separately</button><span data-i18n="scenario.saveHint">Only saved cases can be run.</span></div>
64
+ <details id="technical-details" class="technical-details">
65
+ <summary><span data-i18n="scenario.technical">Edit JSON · Validate · View script</span><span aria-hidden="true">+</span></summary>
66
+ <div class="technical-body">
67
+ <label for="suite-editor" class="field-label" data-i18n="scenario.suiteJson">Suite JSON</label>
68
+ <textarea id="suite-editor" class="code-editor" rows="13" spellcheck="false" placeholder="Load a local demo case from the library or enter suite JSON." data-i18n-placeholder="scenario.suitePlaceholder"></textarea>
69
+ <div class="actions"><button id="new-draft-btn" type="button" class="button quiet" data-i18n="scenario.newDraft">New JSON draft</button><button id="compile-btn" type="button" class="button" data-i18n="scenario.compile">Validate JSON · Generate script</button></div>
70
+ <div class="script-head"><h3 data-i18n="scenario.script">Run script</h3><button type="button" id="script-download" class="text-button" disabled data-i18n="scenario.downloadScript">Download script ↓</button></div>
71
+ <pre id="script-preview" class="script-preview" data-i18n="scenario.scriptPlaceholder">The script for validated cases will appear here.</pre>
72
+ </div>
73
+ </details>
74
+ </div>
75
+ </section>
76
+ <section class="execution-section" aria-labelledby="run-title">
77
+ <div class="section-head"><span class="section-number">03</span><div><h2 id="run-title" data-i18n="scenario.runTitle">Review, then run</h2><p data-i18n="scenario.runHelp">Select a target and confirm you have reviewed the cases.</p></div></div>
78
+ <div class="execution-grid">
79
+ <div class="target-field"><label for="target-select" class="field-label" data-i18n="scenario.target">Run target</label><select id="target-select"><option value="" data-i18n="scenario.selectTarget">Select a target</option></select><p id="target-description" class="hint" data-i18n="scenario.targetHint">You can add a target in Connections.</p></div>
80
+ <div class="execution-confirm"><p id="case-readiness" class="hint"></p><label class="review-line"><input type="checkbox" id="review-checkbox"><span data-i18n="scenario.reviewConfirm">I have reviewed the requirements, case steps, and expected results.</span></label><div class="run-bar"><span id="run-readiness" data-i18n="scenario.runInitial">Validated cases and a run target are required.</span><div class="actions"><button type="button" id="stop-btn" class="button quiet" hidden data-i18n="scenario.stop">Stop run</button><button type="button" id="run-btn" class="button primary" disabled><span data-i18n="scenario.run">Run QA</span><span aria-hidden="true">→</span></button></div></div></div>
81
+ </div>
82
+ </section>
83
+ </div>
84
+ </section>
85
+ <section id="view-library" class="view" hidden>
86
+ <div class="page-heading"><p class="eyebrow" data-i18n="library.eyebrow">02 / LIBRARY</p><h1 data-i18n="nav.library">Case library</h1><p data-i18n="library.intro">Cases saved on disk. Local demo examples are not AI generated results.</p></div>
87
+ <section class="list-section"><div class="section-head"><span class="section-number">01</span><div><h2 data-i18n="library.title">Saved suites</h2><p data-i18n="library.help">Load a suite, then review it on the Scenario page.</p></div></div><div id="library-list" class="list-body"></div></section>
88
+ </section>
89
+ <section id="view-history" class="view" hidden>
90
+ <div class="page-heading"><p class="eyebrow" data-i18n="history.eyebrow">03 / RUNS</p><h1 data-i18n="nav.history">Run history</h1><p data-i18n="history.intro">Only runs that actually started appear here. Once results are saved, you can view the detailed report.</p></div>
91
+ <section class="list-section"><div class="section-head"><span class="section-number">01</span><div><h2 data-i18n="history.title">Runs</h2><p data-i18n="history.help">Check the completed report for the final status.</p></div></div><div id="history-list" class="list-body"></div></section>
92
+ <section id="report-panel" class="report-panel" hidden><div class="section-head"><span class="section-number">02</span><div><h2 data-i18n="history.report">Detailed report</h2><p id="report-heading"></p></div></div><div class="section-body"><div class="actions"><button type="button" id="report-md" class="button" data-i18n="history.downloadMarkdown">Download Markdown</button><button type="button" id="report-json" class="button quiet" data-i18n="history.downloadJson">Download JSON</button></div><p class="hint" data-i18n="history.evidenceHint">The report summarizes results. Open or download screenshots and recordings from each attempt. Evidence files are not included in report downloads.</p><div id="report-content"></div></div></section>
93
+ </section>
94
+ <section id="view-settings" class="view" hidden>
95
+ <div class="page-heading"><p class="eyebrow" data-i18n="settings.eyebrow">04 / CONNECTIONS</p><h1 data-i18n="nav.settings">Connections</h1><p data-i18n="settings.intro">Configure run targets and models separately. Key values are read only from server environment variables.</p></div>
96
+ <div class="runtime-panel"><h2 data-i18n="settings.runtime">Runtime</h2><p id="runtime-status" role="status" data-i18n="settings.runtimeInitial">Checking installation status.</p><p class="hint" data-i18n="settings.runtimeHint">This shows local installation and configuration. Verify target access, sign-in, and model responses with an actual run.</p></div>
97
+ <section id="display-preferences" class="hds-panel preferences-section" aria-labelledby="preferences-title">
98
+ <div class="hds-heading"><div><span class="hds-kicker" data-i18n="settings.displayKicker">DISPLAY</span><h2 id="preferences-title" data-i18n="settings.display">Display</h2></div><p data-i18n="settings.displayHelp">Choose a comfortable display.</p></div>
99
+ <div class="hds-groups">
100
+ <fieldset class="hds-group"><legend data-i18n="top.theme">Theme</legend><div class="hds-options"><label class="hds-radioOption"><input type="radio" name="theme" value="system"><span data-i18n="settings.themeSystem">System</span></label><label class="hds-radioOption"><input type="radio" name="theme" value="light"><span data-i18n="settings.themeLight">Light</span></label><label class="hds-radioOption"><input type="radio" name="theme" value="dark"><span data-i18n="settings.themeDark">Dark</span></label></div></fieldset>
101
+ <fieldset class="hds-group"><legend data-i18n="settings.accent">Accent</legend><div class="hds-accents"><label class="hds-radioOption"><input type="radio" name="accent" value="cobalt"><span data-i18n="settings.accentCobalt">Cobalt</span></label><label class="hds-radioOption"><input type="radio" name="accent" value="forest"><span data-i18n="settings.accentForest">Forest</span></label><label class="hds-radioOption"><input type="radio" name="accent" value="coral"><span data-i18n="settings.accentCoral">Coral</span></label></div></fieldset>
102
+ <fieldset class="hds-group"><legend data-i18n="settings.density">Density</legend><div class="hds-options"><label class="hds-radioOption"><input type="radio" name="density" value="comfortable"><span data-i18n="settings.densityComfortable">Comfortable</span></label><label class="hds-radioOption"><input type="radio" name="density" value="compact"><span data-i18n="settings.densityCompact">Compact</span></label></div></fieldset>
103
+ <fieldset class="hds-group"><legend data-i18n="settings.corners">Corners</legend><div class="hds-options"><label class="hds-radioOption"><input type="radio" name="corners" value="crisp"><span data-i18n="settings.cornersCrisp">Crisp</span></label><label class="hds-radioOption"><input type="radio" name="corners" value="soft"><span data-i18n="settings.cornersSoft">Soft</span></label></div></fieldset>
104
+ <fieldset class="hds-group"><legend data-i18n="settings.motion">Motion</legend><div class="hds-options"><label class="hds-radioOption"><input type="radio" name="motion" value="full"><span data-i18n="settings.motionFull">Full</span></label><label class="hds-radioOption"><input type="radio" name="motion" value="reduced"><span data-i18n="settings.motionReduced">Reduced</span></label></div></fieldset>
105
+ </div>
106
+ <div class="hds-footer"><p id="preferences-status" aria-live="polite">Your display choices are saved in this browser.</p><button id="reset-preferences" type="button" data-i18n="settings.reset">Reset to defaults</button></div>
107
+ </section>
108
+ <div class="settings-grid">
109
+ <section class="settings-section"><div class="section-head"><span class="section-number">01</span><div><h2 data-i18n="settings.targets">Run targets</h2><p data-i18n="settings.targetsHelp">Add an allowed HTTPS or local address.</p></div></div><form class="section-body form-stack" id="target-form"><label><span data-i18n="settings.targetName">Target name</span><input name="name" required placeholder="e.g. local-demo" data-i18n-placeholder="settings.targetNamePlaceholder" pattern="[A-Za-z0-9][A-Za-z0-9_-]*"></label><label>URL<input name="url" type="url" required placeholder="https://example.com"></label><label><span data-i18n="settings.description">Description</span><textarea name="description" rows="3" placeholder="What this target is for" data-i18n-placeholder="settings.descriptionPlaceholder"></textarea></label><button class="button primary" type="submit" data-i18n="settings.saveTarget">Save target</button><div id="target-list" class="settings-list"></div></form></section>
110
+ <section class="settings-section"><div class="section-head"><span class="section-number">02</span><div><h2 data-i18n="settings.models">Model connections</h2><p data-i18n="settings.modelsHelp">Manage planner and run agent connections separately.</p></div></div><div class="section-body"><p class="hint" data-i18n="settings.modelsHint">Do not enter or store API key values here. Specify only the name of an environment variable in the server process.</p><div id="model-forms"></div></div></section>
111
+ </div>
112
+ </section>
113
+ </div>
114
+ </main>
115
+ </div>
116
+ </body>
117
+ </html>