@yudin-s/react-chrome-ai 0.1.0
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.
- package/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/SECURITY.md +12 -0
- package/dist/index.cjs +938 -0
- package/dist/index.d.cts +431 -0
- package/dist/index.d.ts +431 -0
- package/dist/index.js +868 -0
- package/docs/ai-agents.md +155 -0
- package/docs/hooks.md +378 -0
- package/docs/publishing.md +102 -0
- package/examples/README.md +24 -0
- package/examples/basic-prompt.tsx +18 -0
- package/examples/chrome-ai-studio/README.md +22 -0
- package/examples/chrome-ai-studio/index.html +12 -0
- package/examples/chrome-ai-studio/package.json +22 -0
- package/examples/chrome-ai-studio/src/App.tsx +138 -0
- package/examples/chrome-ai-studio/src/main.tsx +10 -0
- package/examples/chrome-ai-studio/src/styles.css +219 -0
- package/examples/chrome-ai-studio/tsconfig.json +16 -0
- package/examples/chrome-ai-studio/vite.config.ts +6 -0
- package/examples/local-review-workbench/README.md +18 -0
- package/examples/local-review-workbench/index.html +12 -0
- package/examples/local-review-workbench/package.json +22 -0
- package/examples/local-review-workbench/src/App.tsx +121 -0
- package/examples/local-review-workbench/src/main.tsx +10 -0
- package/examples/local-review-workbench/src/styles.css +190 -0
- package/examples/local-review-workbench/tsconfig.json +16 -0
- package/examples/local-review-workbench/vite.config.ts +6 -0
- package/examples/local-translator/README.md +24 -0
- package/examples/local-translator/index.html +12 -0
- package/examples/local-translator/package.json +23 -0
- package/examples/local-translator/src/App.tsx +235 -0
- package/examples/local-translator/src/main.tsx +10 -0
- package/examples/local-translator/src/styles.css +161 -0
- package/examples/local-translator/tsconfig.json +16 -0
- package/examples/local-translator/vite.config.ts +6 -0
- package/examples/model-download-progress.tsx +20 -0
- package/examples/summarizer.tsx +28 -0
- package/llms.txt +97 -0
- package/package.json +86 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
background: #f5f7fa;
|
|
3
|
+
color: #182230;
|
|
4
|
+
font-family:
|
|
5
|
+
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
6
|
+
sans-serif;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
* {
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
body {
|
|
14
|
+
margin: 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
button,
|
|
18
|
+
textarea {
|
|
19
|
+
font: inherit;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
button {
|
|
23
|
+
min-height: 38px;
|
|
24
|
+
border: 0;
|
|
25
|
+
border-radius: 6px;
|
|
26
|
+
padding: 0 14px;
|
|
27
|
+
background: #0b6b58;
|
|
28
|
+
color: white;
|
|
29
|
+
font-weight: 700;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
button.secondary {
|
|
33
|
+
background: #475467;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
button:disabled {
|
|
37
|
+
opacity: 0.55;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.shell {
|
|
41
|
+
width: min(1200px, calc(100% - 32px));
|
|
42
|
+
margin: 0 auto;
|
|
43
|
+
padding: 30px 0 48px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
header {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: end;
|
|
49
|
+
justify-content: space-between;
|
|
50
|
+
gap: 20px;
|
|
51
|
+
margin-bottom: 22px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
header p {
|
|
55
|
+
margin: 0;
|
|
56
|
+
color: #667085;
|
|
57
|
+
font-weight: 800;
|
|
58
|
+
text-transform: uppercase;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
h1 {
|
|
62
|
+
margin: 0;
|
|
63
|
+
font-size: clamp(2.1rem, 4vw, 4.3rem);
|
|
64
|
+
line-height: 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
h2,
|
|
68
|
+
h3,
|
|
69
|
+
p {
|
|
70
|
+
margin-top: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.layout {
|
|
74
|
+
display: grid;
|
|
75
|
+
grid-template-columns: 1.15fr 0.85fr;
|
|
76
|
+
gap: 16px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.panel {
|
|
80
|
+
border: 1px solid #d6dde8;
|
|
81
|
+
border-radius: 8px;
|
|
82
|
+
padding: 18px;
|
|
83
|
+
background: white;
|
|
84
|
+
box-shadow: 0 12px 28px rgba(24, 34, 48, 0.06);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.input-panel {
|
|
88
|
+
grid-row: span 2;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.panel-heading,
|
|
92
|
+
.actions {
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
justify-content: space-between;
|
|
96
|
+
gap: 10px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
textarea {
|
|
100
|
+
width: 100%;
|
|
101
|
+
min-height: 430px;
|
|
102
|
+
margin: 12px 0;
|
|
103
|
+
resize: vertical;
|
|
104
|
+
border: 1px solid #cbd5e1;
|
|
105
|
+
border-radius: 6px;
|
|
106
|
+
padding: 14px;
|
|
107
|
+
background: #0f172a;
|
|
108
|
+
color: #dbeafe;
|
|
109
|
+
line-height: 1.5;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
progress {
|
|
113
|
+
width: 100%;
|
|
114
|
+
margin-top: 14px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.risk-strip {
|
|
118
|
+
display: flex;
|
|
119
|
+
justify-content: space-between;
|
|
120
|
+
padding: 12px;
|
|
121
|
+
border-radius: 6px;
|
|
122
|
+
background: #edf4ff;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.risk-strip[data-risk="high"] {
|
|
126
|
+
background: #fff1f3;
|
|
127
|
+
color: #b42318;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.risk-strip[data-risk="medium"] {
|
|
131
|
+
background: #fff7e6;
|
|
132
|
+
color: #b54708;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.risk-strip[data-risk="low"] {
|
|
136
|
+
background: #ecfdf3;
|
|
137
|
+
color: #067647;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.findings {
|
|
141
|
+
display: grid;
|
|
142
|
+
gap: 10px;
|
|
143
|
+
margin-top: 14px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.findings article {
|
|
147
|
+
border: 1px solid #e2e8f0;
|
|
148
|
+
border-radius: 6px;
|
|
149
|
+
padding: 12px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.findings span {
|
|
153
|
+
color: #667085;
|
|
154
|
+
font-size: 0.78rem;
|
|
155
|
+
font-weight: 800;
|
|
156
|
+
text-transform: uppercase;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
output {
|
|
160
|
+
display: block;
|
|
161
|
+
min-height: 180px;
|
|
162
|
+
white-space: pre-wrap;
|
|
163
|
+
border: 1px solid #e2e8f0;
|
|
164
|
+
border-radius: 6px;
|
|
165
|
+
padding: 12px;
|
|
166
|
+
background: #f8fafc;
|
|
167
|
+
line-height: 1.55;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.muted {
|
|
171
|
+
color: #667085;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
[role="alert"] {
|
|
175
|
+
color: #b42318;
|
|
176
|
+
font-weight: 700;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@media (max-width: 900px) {
|
|
180
|
+
header,
|
|
181
|
+
.panel-heading,
|
|
182
|
+
.actions {
|
|
183
|
+
align-items: stretch;
|
|
184
|
+
flex-direction: column;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.layout {
|
|
188
|
+
grid-template-columns: 1fr;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2021",
|
|
4
|
+
"lib": ["DOM", "DOM.Iterable", "ES2021"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"isolatedModules": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src", "vite.config.ts"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Local Translator (Vite + React)
|
|
2
|
+
|
|
3
|
+
Self-contained example showing:
|
|
4
|
+
|
|
5
|
+
- `useChromeAITranslator`
|
|
6
|
+
- `useChromeAILanguageDetector`
|
|
7
|
+
- support/availability/status/progress/error diagnostics for both APIs
|
|
8
|
+
- source/target language controls
|
|
9
|
+
- textarea input
|
|
10
|
+
- Detect + Translate actions
|
|
11
|
+
- output panel
|
|
12
|
+
|
|
13
|
+
## Run
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd examples/local-translator
|
|
17
|
+
npm install
|
|
18
|
+
npm run dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Notes
|
|
22
|
+
|
|
23
|
+
This example targets Chrome Built-in AI APIs (if available in your browser).
|
|
24
|
+
If translator/detector are unavailable, the cards show availability and status states directly from hooks.
|
|
@@ -0,0 +1,12 @@
|
|
|
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.0" />
|
|
6
|
+
<title>Local Translator</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "local-translator-vite-example",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc --noEmit && vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
13
|
+
"@yudin-s/react-chrome-ai": "file:../..",
|
|
14
|
+
"vite": "^6.0.7",
|
|
15
|
+
"typescript": "^5.7.2",
|
|
16
|
+
"react": "^19.0.0",
|
|
17
|
+
"react-dom": "^19.0.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/react": "^19.0.0",
|
|
21
|
+
"@types/react-dom": "^19.0.2"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { FormEvent, useMemo, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
useChromeAITranslator,
|
|
4
|
+
useChromeAILanguageDetector,
|
|
5
|
+
} from "@yudin-s/react-chrome-ai";
|
|
6
|
+
|
|
7
|
+
type AsyncState = "idle" | "checking" | "unsupported" | "unavailable" | "downloadable" | "downloading" | "preparing" | "ready" | "prompting" | "streaming" | "aborted" | "error";
|
|
8
|
+
|
|
9
|
+
function normalizeLanguage(value: unknown): string | null {
|
|
10
|
+
if (typeof value === "string" && value.trim()) {
|
|
11
|
+
return value.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
for (const item of value) {
|
|
16
|
+
const candidate = normalizeLanguage(item);
|
|
17
|
+
if (candidate) {
|
|
18
|
+
return candidate;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (value && typeof value === "object") {
|
|
25
|
+
const record = value as Record<string, unknown>;
|
|
26
|
+
for (const key of ["language", "detectedLanguage", "lang", "locale"]) {
|
|
27
|
+
const candidate = normalizeLanguage(record[key]);
|
|
28
|
+
if (candidate) {
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function statusToPercent(state: number | undefined, progress?: { progress?: number }) {
|
|
38
|
+
if (state != null) {
|
|
39
|
+
return Math.min(100, Math.max(0, Math.round(state * 100)));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (progress?.progress == null) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return Math.min(100, Math.max(0, Math.round(progress.progress * 100)));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ApiDiagnostic({
|
|
50
|
+
title,
|
|
51
|
+
supported,
|
|
52
|
+
availability,
|
|
53
|
+
status,
|
|
54
|
+
progress,
|
|
55
|
+
error,
|
|
56
|
+
}: {
|
|
57
|
+
title: string;
|
|
58
|
+
supported: boolean;
|
|
59
|
+
availability?: string;
|
|
60
|
+
status: AsyncState;
|
|
61
|
+
progress?: { progress?: number; indeterminate?: boolean; completed?: boolean };
|
|
62
|
+
error?: Error;
|
|
63
|
+
}) {
|
|
64
|
+
const percent = statusToPercent(progress?.progress, progress);
|
|
65
|
+
const progressText = percent == null ? "in progress" : `${percent}%`;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<section className="api-card">
|
|
69
|
+
<h2>{title}</h2>
|
|
70
|
+
<div className="kv-grid">
|
|
71
|
+
<div>
|
|
72
|
+
<strong>Support</strong>
|
|
73
|
+
<p>{supported ? "Yes" : "No"}</p>
|
|
74
|
+
</div>
|
|
75
|
+
<div>
|
|
76
|
+
<strong>Availability</strong>
|
|
77
|
+
<p>{availability ?? "unknown"}</p>
|
|
78
|
+
</div>
|
|
79
|
+
<div>
|
|
80
|
+
<strong>Status</strong>
|
|
81
|
+
<p>{status}</p>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
{(status === "downloading" || status === "preparing") && (
|
|
85
|
+
<div className="progress-row">
|
|
86
|
+
<progress max={100} value={percent ?? undefined} />
|
|
87
|
+
<span>{progressText}</span>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
{error && <p className="error">Error: {error.message}</p>}
|
|
91
|
+
</section>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function App() {
|
|
96
|
+
const [sourceText, setSourceText] = useState("Hello. This is a local translator demo.");
|
|
97
|
+
const [sourceLanguage, setSourceLanguage] = useState("en");
|
|
98
|
+
const [targetLanguage, setTargetLanguage] = useState("es");
|
|
99
|
+
const [detectedLanguage, setDetectedLanguage] = useState("");
|
|
100
|
+
|
|
101
|
+
const translator = useChromeAITranslator({
|
|
102
|
+
createOptions: {
|
|
103
|
+
sourceLanguage: sourceLanguage || "en",
|
|
104
|
+
targetLanguage: targetLanguage || "en",
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const detector = useChromeAILanguageDetector();
|
|
109
|
+
const isBusy = translator.status === "prompting" || translator.status === "streaming";
|
|
110
|
+
const isDetecting = detector.status === "prompting" || detector.status === "streaming";
|
|
111
|
+
const translatedValue = useMemo(() => translator.text.trim(), [translator.text]);
|
|
112
|
+
|
|
113
|
+
const onSubmit = (event: FormEvent) => {
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
void onTranslate();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const onDetect = async () => {
|
|
119
|
+
if (!sourceText.trim()) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const detected = await detector.run(sourceText);
|
|
123
|
+
const next = normalizeLanguage(detected);
|
|
124
|
+
if (next) {
|
|
125
|
+
setDetectedLanguage(next);
|
|
126
|
+
setSourceLanguage(next);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
setDetectedLanguage("Not detected");
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const onTranslate = async () => {
|
|
133
|
+
if (!sourceText.trim()) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
await translator.run(sourceText);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<main className="page">
|
|
141
|
+
<header className="hero">
|
|
142
|
+
<h1>Local Translator Demo</h1>
|
|
143
|
+
<p>Chrome AI Translator + LanguageDetector, working in-browser.</p>
|
|
144
|
+
</header>
|
|
145
|
+
|
|
146
|
+
<div className="grid">
|
|
147
|
+
<ApiDiagnostic
|
|
148
|
+
title="Translator API"
|
|
149
|
+
supported={translator.supported}
|
|
150
|
+
availability={translator.availability}
|
|
151
|
+
status={translator.status}
|
|
152
|
+
progress={translator.progress}
|
|
153
|
+
error={translator.error}
|
|
154
|
+
/>
|
|
155
|
+
<ApiDiagnostic
|
|
156
|
+
title="LanguageDetector API"
|
|
157
|
+
supported={detector.supported}
|
|
158
|
+
availability={detector.availability}
|
|
159
|
+
status={detector.status}
|
|
160
|
+
progress={detector.progress}
|
|
161
|
+
error={detector.error}
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<section className="card">
|
|
166
|
+
<form className="form" onSubmit={onSubmit}>
|
|
167
|
+
<label className="inline-field">
|
|
168
|
+
<span>Source language</span>
|
|
169
|
+
<input
|
|
170
|
+
value={sourceLanguage}
|
|
171
|
+
onChange={(event) => setSourceLanguage(event.target.value)}
|
|
172
|
+
placeholder="en"
|
|
173
|
+
aria-label="Source language"
|
|
174
|
+
/>
|
|
175
|
+
</label>
|
|
176
|
+
<label className="inline-field">
|
|
177
|
+
<span>Target language</span>
|
|
178
|
+
<input
|
|
179
|
+
value={targetLanguage}
|
|
180
|
+
onChange={(event) => setTargetLanguage(event.target.value)}
|
|
181
|
+
placeholder="es"
|
|
182
|
+
aria-label="Target language"
|
|
183
|
+
/>
|
|
184
|
+
</label>
|
|
185
|
+
<label className="input-block">
|
|
186
|
+
<span>Source text</span>
|
|
187
|
+
<textarea
|
|
188
|
+
value={sourceText}
|
|
189
|
+
onChange={(event) => setSourceText(event.target.value)}
|
|
190
|
+
placeholder="Введите текст для перевода"
|
|
191
|
+
aria-label="Source text"
|
|
192
|
+
/>
|
|
193
|
+
</label>
|
|
194
|
+
<div className="actions">
|
|
195
|
+
<button type="button" onClick={onDetect} disabled={isDetecting || isBusy}>
|
|
196
|
+
Detect language
|
|
197
|
+
</button>
|
|
198
|
+
<button type="submit" disabled={isBusy || isDetecting}>
|
|
199
|
+
Translate
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
</form>
|
|
203
|
+
|
|
204
|
+
<div className="kv">
|
|
205
|
+
<span>Detected language:</span>
|
|
206
|
+
<strong>{detectedLanguage || "—"}</strong>
|
|
207
|
+
</div>
|
|
208
|
+
<div className="kv">
|
|
209
|
+
<span>Output language:</span>
|
|
210
|
+
<strong>{targetLanguage || "—"}</strong>
|
|
211
|
+
</div>
|
|
212
|
+
</section>
|
|
213
|
+
|
|
214
|
+
<section className="card">
|
|
215
|
+
<h2>Output</h2>
|
|
216
|
+
<output className="output">
|
|
217
|
+
{translator.status === "prompting" || translator.status === "streaming"
|
|
218
|
+
? "Translating..."
|
|
219
|
+
: translatedValue || "No output yet"}
|
|
220
|
+
</output>
|
|
221
|
+
</section>
|
|
222
|
+
|
|
223
|
+
{translator.error && (
|
|
224
|
+
<p role="alert" className="error">
|
|
225
|
+
Translate error: {translator.error.message}
|
|
226
|
+
</p>
|
|
227
|
+
)}
|
|
228
|
+
{detector.error && (
|
|
229
|
+
<p role="alert" className="error">
|
|
230
|
+
Detect error: {detector.error.message}
|
|
231
|
+
</p>
|
|
232
|
+
)}
|
|
233
|
+
</main>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light;
|
|
3
|
+
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
* {
|
|
7
|
+
box-sizing: border-box;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
margin: 0;
|
|
12
|
+
min-height: 100vh;
|
|
13
|
+
background: #f6f7fb;
|
|
14
|
+
color: #0f172a;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#root {
|
|
18
|
+
max-width: 960px;
|
|
19
|
+
margin: 0 auto;
|
|
20
|
+
padding: 2rem 1rem 3rem;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.page {
|
|
24
|
+
display: grid;
|
|
25
|
+
gap: 1rem;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.hero h1 {
|
|
29
|
+
margin: 0;
|
|
30
|
+
font-size: 1.8rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.hero p {
|
|
34
|
+
margin: 0.5rem 0 0;
|
|
35
|
+
color: #334155;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.grid {
|
|
39
|
+
display: grid;
|
|
40
|
+
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
41
|
+
gap: 1rem;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.card,
|
|
45
|
+
.api-card {
|
|
46
|
+
background: #ffffff;
|
|
47
|
+
border: 1px solid #d4d8e3;
|
|
48
|
+
border-radius: 8px;
|
|
49
|
+
padding: 1rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
h2 {
|
|
53
|
+
margin: 0 0 0.75rem;
|
|
54
|
+
font-size: 1.1rem;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.kv-grid {
|
|
58
|
+
display: grid;
|
|
59
|
+
gap: 0.75rem;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.kv-grid > div {
|
|
63
|
+
display: grid;
|
|
64
|
+
gap: 0.25rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.kv-grid p {
|
|
68
|
+
margin: 0;
|
|
69
|
+
color: #334155;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.progress-row {
|
|
73
|
+
margin-top: 0.75rem;
|
|
74
|
+
display: grid;
|
|
75
|
+
gap: 0.5rem;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
progress {
|
|
79
|
+
width: 100%;
|
|
80
|
+
height: 0.6rem;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.form {
|
|
84
|
+
display: grid;
|
|
85
|
+
gap: 0.75rem;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.inline-field,
|
|
89
|
+
.input-block {
|
|
90
|
+
display: grid;
|
|
91
|
+
gap: 0.35rem;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
input,
|
|
95
|
+
textarea {
|
|
96
|
+
border: 1px solid #cfd8e3;
|
|
97
|
+
border-radius: 6px;
|
|
98
|
+
padding: 0.6rem 0.65rem;
|
|
99
|
+
font: inherit;
|
|
100
|
+
color: inherit;
|
|
101
|
+
background: #fff;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
textarea {
|
|
105
|
+
min-height: 150px;
|
|
106
|
+
resize: vertical;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.actions {
|
|
110
|
+
display: flex;
|
|
111
|
+
gap: 0.6rem;
|
|
112
|
+
flex-wrap: wrap;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
button {
|
|
116
|
+
border: 1px solid #1e293b;
|
|
117
|
+
color: #f8fafc;
|
|
118
|
+
background: #1e293b;
|
|
119
|
+
border-radius: 6px;
|
|
120
|
+
padding: 0.58rem 0.95rem;
|
|
121
|
+
font: inherit;
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
button:hover {
|
|
127
|
+
background: #0f172a;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
button:disabled {
|
|
131
|
+
cursor: not-allowed;
|
|
132
|
+
opacity: 0.6;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.kv {
|
|
136
|
+
display: grid;
|
|
137
|
+
grid-template-columns: 1fr 1fr;
|
|
138
|
+
gap: 0.3rem;
|
|
139
|
+
margin-top: 0.85rem;
|
|
140
|
+
padding-top: 0.85rem;
|
|
141
|
+
border-top: 1px solid #e2e8f0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.kv span {
|
|
145
|
+
color: #475569;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.output {
|
|
149
|
+
display: block;
|
|
150
|
+
min-height: 130px;
|
|
151
|
+
white-space: pre-wrap;
|
|
152
|
+
background: #f8fafc;
|
|
153
|
+
border: 1px solid #d4d8e3;
|
|
154
|
+
border-radius: 6px;
|
|
155
|
+
padding: 0.8rem;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.error {
|
|
159
|
+
color: #b91c1c;
|
|
160
|
+
margin: 0.35rem 0 0;
|
|
161
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2021",
|
|
4
|
+
"lib": ["DOM", "DOM.Iterable", "ES2021"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"isolatedModules": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src", "vite.config.ts"]
|
|
16
|
+
}
|